|
2740
|
111
|
41
|
2026-05-07T11:38:05.752756+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153885752_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2742
|
111
|
42
|
2026-05-07T11:38:08.024805+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153888024_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-05-07 11:36:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-opportunity","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:45] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 11:36:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-opportunity\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:45] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":6,\"total_elapsed_seconds\":0.48,\"average_seconds_per_request\":0.48} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:46] local.INFO: [HubSpot] importOpportunityBatch timing {\"teamId\":2,\"deal_count\":6,\"total_ms\":659,\"company_assoc_api_ms\":276,\"contact_assoc_api_ms\":203,\"prepare_entities_ms\":19,\"prepare_accounts_ms\":3,\"prepare_contacts_ms\":17,\"total_companies\":5,\"missing_companies\":0,\"total_contacts\":23,\"missing_contacts\":0,\"deals_loop_ms\":152,\"avg_deal_ms\":25,\"slow_deals_count\":0,\"slow_deals\":[]} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:46] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":5,\"total\":6,\"last_synced_id\":\"297846423\",\"duration_ms\":1161.39} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-opportunity\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":176.5,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:37:05] 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\":\"34ae70b9-ee92-4e0c-bb97-3ecc204278be\",\"trace_id\":\"0d02e69d-5e33-4059-938c-d41350a39b72\"}\n[2026-05-07 11:37:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"34ae70b9-ee92-4e0c-bb97-3ecc204278be\",\"trace_id\":\"0d02e69d-5e33-4059-938c-d41350a39b72\"}\n[2026-05-07 11:37:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"34ae70b9-ee92-4e0c-bb97-3ecc204278be\",\"trace_id\":\"0d02e69d-5e33-4059-938c-d41350a39b72\"}\n[2026-05-07 11:37:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6052f7ad-dde1-41de-8051-5ba8d54babdd\",\"trace_id\":\"2e216848-ddf9-4f8c-8808-93a09cef8f5f\"}\n[2026-05-07 11:37:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6052f7ad-dde1-41de-8051-5ba8d54babdd\",\"trace_id\":\"2e216848-ddf9-4f8c-8808-93a09cef8f5f\"}\n[2026-05-07 11:37:07] local.NOTICE: Monitoring start {\"correlation_id\":\"64decce4-a339-4c1b-8a73-4f612ef2eea2\",\"trace_id\":\"07e59e6c-d681-41f9-ba04-5cab00e9a754\"}\n[2026-05-07 11:37:07] local.NOTICE: Monitoring end {\"correlation_id\":\"64decce4-a339-4c1b-8a73-4f612ef2eea2\",\"trace_id\":\"07e59e6c-d681-41f9-ba04-5cab00e9a754\"}\n[2026-05-07 11:37:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0326b1c8-69ac-4d09-aae5-e808abc71f46\",\"trace_id\":\"2d16668b-26b0-44cf-a533-493500255667\"}\n[2026-05-07 11:37:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0326b1c8-69ac-4d09-aae5-e808abc71f46\",\"trace_id\":\"2d16668b-26b0-44cf-a533-493500255667\"}\n[2026-05-07 11:37:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:10] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:10] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:13] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:13] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e5f2a44-295d-4c28-b5e9-1a649c1f49ae\",\"trace_id\":\"f4d85ec2-b1d2-4418-bb65-814c209e628f\"}\n[2026-05-07 11:37:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e5f2a44-295d-4c28-b5e9-1a649c1f49ae\",\"trace_id\":\"f4d85ec2-b1d2-4418-bb65-814c209e628f\"}\n[2026-05-07 11:37:21] local.INFO: [Commands/AsyncUpdateEsEntities] Starting ES update worker {\"pid\":20519,\"workerId\":\"\",\"target\":\"activities\"} {\"correlation_id\":\"1a81e470-75ac-4728-9d45-9f879fc96c5b\",\"trace_id\":\"7184ea88-9f01-42bc-9508-8e4ad0c1204b\"}\n[2026-05-07 11:37:22] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"798a3e91-233c-4b61-aa8e-80a7b84ac45f\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:53] local.INFO: [Commands/AsyncUpdateEsEntities] Starting ES update worker {\"pid\":20661,\"workerId\":\"\",\"target\":\"activities\"} {\"correlation_id\":\"293fa4a8-dc12-452e-88a3-f7408f8ed676\",\"trace_id\":\"e3d77ea8-2ff3-4b4e-a388-8f21192e51f3\"}\n[2026-05-07 11:38:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-opportunity\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":6,\"total_elapsed_seconds\":0.56,\"average_seconds_per_request\":0.56} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}","depth":4,"on_screen":true,"value":"[2026-05-07 11:36:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-opportunity\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:44] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:45] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":6,\"total_elapsed_seconds\":0.48,\"average_seconds_per_request\":0.48} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:46] local.INFO: [HubSpot] importOpportunityBatch timing {\"teamId\":2,\"deal_count\":6,\"total_ms\":659,\"company_assoc_api_ms\":276,\"contact_assoc_api_ms\":203,\"prepare_entities_ms\":19,\"prepare_accounts_ms\":3,\"prepare_contacts_ms\":17,\"total_companies\":5,\"missing_companies\":0,\"total_contacts\":23,\"missing_contacts\":0,\"deals_loop_ms\":152,\"avg_deal_ms\":25,\"slow_deals_count\":0,\"slow_deals\":[]} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:46] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":5,\"total\":6,\"last_synced_id\":\"297846423\",\"duration_ms\":1161.39} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:46] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-opportunity\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"462baf35-d312-4641-9896-e8b15e8fdb82\",\"trace_id\":\"aeb070fe-166a-48bf-99c0-acfcbfb40c1b\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":176.5,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:36:52] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"8568abe7-be33-417c-aa2f-b1bd53fb4adc\",\"trace_id\":\"582bff07-5ea4-4469-8ca8-efa41b04d46d\"}\n[2026-05-07 11:37:05] 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\":\"34ae70b9-ee92-4e0c-bb97-3ecc204278be\",\"trace_id\":\"0d02e69d-5e33-4059-938c-d41350a39b72\"}\n[2026-05-07 11:37:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"34ae70b9-ee92-4e0c-bb97-3ecc204278be\",\"trace_id\":\"0d02e69d-5e33-4059-938c-d41350a39b72\"}\n[2026-05-07 11:37:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"34ae70b9-ee92-4e0c-bb97-3ecc204278be\",\"trace_id\":\"0d02e69d-5e33-4059-938c-d41350a39b72\"}\n[2026-05-07 11:37:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6052f7ad-dde1-41de-8051-5ba8d54babdd\",\"trace_id\":\"2e216848-ddf9-4f8c-8808-93a09cef8f5f\"}\n[2026-05-07 11:37:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6052f7ad-dde1-41de-8051-5ba8d54babdd\",\"trace_id\":\"2e216848-ddf9-4f8c-8808-93a09cef8f5f\"}\n[2026-05-07 11:37:07] local.NOTICE: Monitoring start {\"correlation_id\":\"64decce4-a339-4c1b-8a73-4f612ef2eea2\",\"trace_id\":\"07e59e6c-d681-41f9-ba04-5cab00e9a754\"}\n[2026-05-07 11:37:07] local.NOTICE: Monitoring end {\"correlation_id\":\"64decce4-a339-4c1b-8a73-4f612ef2eea2\",\"trace_id\":\"07e59e6c-d681-41f9-ba04-5cab00e9a754\"}\n[2026-05-07 11:37:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0326b1c8-69ac-4d09-aae5-e808abc71f46\",\"trace_id\":\"2d16668b-26b0-44cf-a533-493500255667\"}\n[2026-05-07 11:37:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0326b1c8-69ac-4d09-aae5-e808abc71f46\",\"trace_id\":\"2d16668b-26b0-44cf-a533-493500255667\"}\n[2026-05-07 11:37:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:10] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:10] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"aa512302-070c-4f99-b3ef-1a43c71b6297\",\"trace_id\":\"9e5c4036-6a42-4c64-a9b3-f11143254293\"}\n[2026-05-07 11:37:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:13] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:13] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b3d4318e-3db2-413c-8fe1-8827ec834020\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e5f2a44-295d-4c28-b5e9-1a649c1f49ae\",\"trace_id\":\"f4d85ec2-b1d2-4418-bb65-814c209e628f\"}\n[2026-05-07 11:37:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e5f2a44-295d-4c28-b5e9-1a649c1f49ae\",\"trace_id\":\"f4d85ec2-b1d2-4418-bb65-814c209e628f\"}\n[2026-05-07 11:37:21] local.INFO: [Commands/AsyncUpdateEsEntities] Starting ES update worker {\"pid\":20519,\"workerId\":\"\",\"target\":\"activities\"} {\"correlation_id\":\"1a81e470-75ac-4728-9d45-9f879fc96c5b\",\"trace_id\":\"7184ea88-9f01-42bc-9508-8e4ad0c1204b\"}\n[2026-05-07 11:37:22] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"798a3e91-233c-4b61-aa8e-80a7b84ac45f\",\"trace_id\":\"870e454c-3a47-419e-88f1-2321d38b8937\"}\n[2026-05-07 11:37:53] local.INFO: [Commands/AsyncUpdateEsEntities] Starting ES update worker {\"pid\":20661,\"workerId\":\"\",\"target\":\"activities\"} {\"correlation_id\":\"293fa4a8-dc12-452e-88a3-f7408f8ed676\",\"trace_id\":\"e3d77ea8-2ff3-4b4e-a388-8f21192e51f3\"}\n[2026-05-07 11:38:03] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-opportunity\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}\n[2026-05-07 11:38:04] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":6,\"total_elapsed_seconds\":0.56,\"average_seconds_per_request\":0.56} {\"correlation_id\":\"eb6c1736-f06b-4fbc-80f1-e1283f2021fb\",\"trace_id\":\"cef5d248-2fbf-443a-915a-81470cec6a21\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2199466652252128033
|
6666989725209335916
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-05-07 11:36:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-opportunity","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:44] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"462baf35-d312-4641-9896-e8b15e8fdb82","trace_id":"aeb070fe-166a-48bf-99c0-acfcbfb40c1b"}
[2026-05-07 11:36:45] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2740
|
NULL
|
NULL
|
NULL
|
|
2744
|
111
|
43
|
2026-05-07T11:38:09.493606+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153889493_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2748
|
111
|
44
|
2026-05-07T11:38:28.609738+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153908609_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncOpportunity.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Opportunity;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Log\LoggerInterface;
class SyncOpportunity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
private string $opportunityId;
public function __construct(Opportunity $opportunity)
{
$this->opportunityId = $opportunity->id_string;
$this->onQueue(Constants::QUEUE_CRM_SYNC);
}
/**
* Sync CRM object data to ensure we have an up-to-date copy.
*/
public function handle(
ResolveTeamCrmConnection $resolveTeamCrmConnection,
LoggerInterface $logger,
OpportunityRepository $opportunityRepository
): void {
$opportunity = $opportunityRepository->findByUuid($this->opportunityId);
if (! $opportunity instanceof Opportunity) {
$logger->error(__METHOD__ . ': Opportunity not found', [
'opportunity' => $this->opportunityId,
]);
return;
}
$crmConfig = $opportunity->crm;
try {
$crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());
$logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunity', [
'opportunityId' => $this->opportunityId,
]);
$crmService->syncOpportunity($opportunity->crm_provider_id);
} catch (SocialAccountTokenInvalidException $exception) {
// Their account is disconnected, and we are unable to perform this job.
$logger->warning('[' . $crmConfig->getProviderName() . '] Failed to sync opportunity', [
'opportunityId' => $this->opportunityId,
'reason' => $exception->getMessage(),
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","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\\Jobs\\Crm;\n\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Log\\LoggerInterface;\n\nclass SyncOpportunity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n private string $opportunityId;\n\n public function __construct(Opportunity $opportunity)\n {\n $this->opportunityId = $opportunity->id_string;\n\n $this->onQueue(Constants::QUEUE_CRM_SYNC);\n }\n\n /**\n * Sync CRM object data to ensure we have an up-to-date copy.\n */\n public function handle(\n ResolveTeamCrmConnection $resolveTeamCrmConnection,\n LoggerInterface $logger,\n OpportunityRepository $opportunityRepository\n ): void {\n $opportunity = $opportunityRepository->findByUuid($this->opportunityId);\n\n if (! $opportunity instanceof Opportunity) {\n $logger->error(__METHOD__ . ': Opportunity not found', [\n 'opportunity' => $this->opportunityId,\n ]);\n\n return;\n }\n\n $crmConfig = $opportunity->crm;\n\n try {\n $crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());\n\n $logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunity', [\n 'opportunityId' => $this->opportunityId,\n ]);\n\n $crmService->syncOpportunity($opportunity->crm_provider_id);\n } catch (SocialAccountTokenInvalidException $exception) {\n // Their account is disconnected, and we are unable to perform this job.\n $logger->warning('[' . $crmConfig->getProviderName() . '] Failed to sync opportunity', [\n 'opportunityId' => $this->opportunityId,\n 'reason' => $exception->getMessage(),\n ]);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Crm;\n\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Job;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Repositories\\Crm\\OpportunityRepository;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Log\\LoggerInterface;\n\nclass SyncOpportunity extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n private string $opportunityId;\n\n public function __construct(Opportunity $opportunity)\n {\n $this->opportunityId = $opportunity->id_string;\n\n $this->onQueue(Constants::QUEUE_CRM_SYNC);\n }\n\n /**\n * Sync CRM object data to ensure we have an up-to-date copy.\n */\n public function handle(\n ResolveTeamCrmConnection $resolveTeamCrmConnection,\n LoggerInterface $logger,\n OpportunityRepository $opportunityRepository\n ): void {\n $opportunity = $opportunityRepository->findByUuid($this->opportunityId);\n\n if (! $opportunity instanceof Opportunity) {\n $logger->error(__METHOD__ . ': Opportunity not found', [\n 'opportunity' => $this->opportunityId,\n ]);\n\n return;\n }\n\n $crmConfig = $opportunity->crm;\n\n try {\n $crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());\n\n $logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunity', [\n 'opportunityId' => $this->opportunityId,\n ]);\n\n $crmService->syncOpportunity($opportunity->crm_provider_id);\n } catch (SocialAccountTokenInvalidException $exception) {\n // Their account is disconnected, and we are unable to perform this job.\n $logger->warning('[' . $crmConfig->getProviderName() . '] Failed to sync opportunity', [\n 'opportunityId' => $this->opportunityId,\n 'reason' => $exception->getMessage(),\n ]);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
908213374935449775
|
-1896522874815976721
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Job;
use Jiminny\Models\Opportunity;
use Jiminny\Repositories\Crm\OpportunityRepository;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Log\LoggerInterface;
class SyncOpportunity extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
private string $opportunityId;
public function __construct(Opportunity $opportunity)
{
$this->opportunityId = $opportunity->id_string;
$this->onQueue(Constants::QUEUE_CRM_SYNC);
}
/**
* Sync CRM object data to ensure we have an up-to-date copy.
*/
public function handle(
ResolveTeamCrmConnection $resolveTeamCrmConnection,
LoggerInterface $logger,
OpportunityRepository $opportunityRepository
): void {
$opportunity = $opportunityRepository->findByUuid($this->opportunityId);
if (! $opportunity instanceof Opportunity) {
$logger->error(__METHOD__ . ': Opportunity not found', [
'opportunity' => $this->opportunityId,
]);
return;
}
$crmConfig = $opportunity->crm;
try {
$crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());
$logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunity', [
'opportunityId' => $this->opportunityId,
]);
$crmService->syncOpportunity($opportunity->crm_provider_id);
} catch (SocialAccountTokenInvalidException $exception) {
// Their account is disconnected, and we are unable to perform this job.
$logger->warning('[' . $crmConfig->getProviderName() . '] Failed to sync opportunity', [
'opportunityId' => $this->opportunityId,
'reason' => $exception->getMessage(),
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2744
|
NULL
|
NULL
|
NULL
|
|
2751
|
111
|
45
|
2026-05-07T11:38:55.028924+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153935028_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncOpportunitiesJob.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Job;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Log\LoggerInterface;
class SyncOpportunitiesJob extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
private int $crmConfigurationId;
private array $parameters;
private ?string $strategy;
public function __construct(int $crmConfigurationId, array $parameters, ?string $strategy = null)
{
$this->crmConfigurationId = $crmConfigurationId;
$this->parameters = $parameters;
$this->strategy = $strategy;
$this->onQueue(Constants::QUEUE_CRM_SYNC);
}
public function handle(
ResolveTeamCrmConnection $resolveTeamCrmConnection,
LoggerInterface $logger,
CrmConfigurationRepository $crmConfigurationRepository
): void {
$startTime = microtime(true);
$crmConfig = $crmConfigurationRepository->findById($this->crmConfigurationId);
if ($crmConfig === null) {
$logger->error('[SyncOpportunitiesJob] CRM configuration not found', [
'crmConfigurationId' => $this->crmConfigurationId,
]);
return;
}
$crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());
$logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities', [
'team' => $crmConfig->getTeam()->getId(),
'parameters' => $this->parameters,
'strategy' => $this->strategy,
]);
$crmService->syncOpportunities($this->parameters, $this->strategy);
$durationMs = round((microtime(true) - $startTime) * 1000, 2);
$logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities finished', [
'team' => $crmConfig->getTeam()->getId(),
'strategy' => $this->strategy,
'duration_ms' => $durationMs,
]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","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\\Jobs\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Job;\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Log\\LoggerInterface;\n\nclass SyncOpportunitiesJob extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n private int $crmConfigurationId;\n private array $parameters;\n private ?string $strategy;\n\n public function __construct(int $crmConfigurationId, array $parameters, ?string $strategy = null)\n {\n $this->crmConfigurationId = $crmConfigurationId;\n $this->parameters = $parameters;\n $this->strategy = $strategy;\n\n $this->onQueue(Constants::QUEUE_CRM_SYNC);\n }\n\n public function handle(\n ResolveTeamCrmConnection $resolveTeamCrmConnection,\n LoggerInterface $logger,\n CrmConfigurationRepository $crmConfigurationRepository\n ): void {\n $startTime = microtime(true);\n $crmConfig = $crmConfigurationRepository->findById($this->crmConfigurationId);\n\n if ($crmConfig === null) {\n $logger->error('[SyncOpportunitiesJob] CRM configuration not found', [\n 'crmConfigurationId' => $this->crmConfigurationId,\n ]);\n\n return;\n }\n\n $crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());\n\n $logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities', [\n 'team' => $crmConfig->getTeam()->getId(),\n 'parameters' => $this->parameters,\n 'strategy' => $this->strategy,\n ]);\n\n $crmService->syncOpportunities($this->parameters, $this->strategy);\n\n $durationMs = round((microtime(true) - $startTime) * 1000, 2);\n $logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities finished', [\n 'team' => $crmConfig->getTeam()->getId(),\n 'strategy' => $this->strategy,\n 'duration_ms' => $durationMs,\n ]);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Component\\Queue\\Constants;\nuse Jiminny\\Jobs\\Job;\nuse Illuminate\\Queue\\SerializesModels;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Jiminny\\Repositories\\Crm\\CrmConfigurationRepository;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Log\\LoggerInterface;\n\nclass SyncOpportunitiesJob extends Job implements ShouldQueue\n{\n use InteractsWithQueue;\n use SerializesModels;\n\n private int $crmConfigurationId;\n private array $parameters;\n private ?string $strategy;\n\n public function __construct(int $crmConfigurationId, array $parameters, ?string $strategy = null)\n {\n $this->crmConfigurationId = $crmConfigurationId;\n $this->parameters = $parameters;\n $this->strategy = $strategy;\n\n $this->onQueue(Constants::QUEUE_CRM_SYNC);\n }\n\n public function handle(\n ResolveTeamCrmConnection $resolveTeamCrmConnection,\n LoggerInterface $logger,\n CrmConfigurationRepository $crmConfigurationRepository\n ): void {\n $startTime = microtime(true);\n $crmConfig = $crmConfigurationRepository->findById($this->crmConfigurationId);\n\n if ($crmConfig === null) {\n $logger->error('[SyncOpportunitiesJob] CRM configuration not found', [\n 'crmConfigurationId' => $this->crmConfigurationId,\n ]);\n\n return;\n }\n\n $crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());\n\n $logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities', [\n 'team' => $crmConfig->getTeam()->getId(),\n 'parameters' => $this->parameters,\n 'strategy' => $this->strategy,\n ]);\n\n $crmService->syncOpportunities($this->parameters, $this->strategy);\n\n $durationMs = round((microtime(true) - $startTime) * 1000, 2);\n $logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities finished', [\n 'team' => $crmConfig->getTeam()->getId(),\n 'strategy' => $this->strategy,\n 'duration_ms' => $durationMs,\n ]);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1407994949500831869
|
-4200101383188084283
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
1
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Component\Queue\Constants;
use Jiminny\Jobs\Job;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Jiminny\Repositories\Crm\CrmConfigurationRepository;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Log\LoggerInterface;
class SyncOpportunitiesJob extends Job implements ShouldQueue
{
use InteractsWithQueue;
use SerializesModels;
private int $crmConfigurationId;
private array $parameters;
private ?string $strategy;
public function __construct(int $crmConfigurationId, array $parameters, ?string $strategy = null)
{
$this->crmConfigurationId = $crmConfigurationId;
$this->parameters = $parameters;
$this->strategy = $strategy;
$this->onQueue(Constants::QUEUE_CRM_SYNC);
}
public function handle(
ResolveTeamCrmConnection $resolveTeamCrmConnection,
LoggerInterface $logger,
CrmConfigurationRepository $crmConfigurationRepository
): void {
$startTime = microtime(true);
$crmConfig = $crmConfigurationRepository->findById($this->crmConfigurationId);
if ($crmConfig === null) {
$logger->error('[SyncOpportunitiesJob] CRM configuration not found', [
'crmConfigurationId' => $this->crmConfigurationId,
]);
return;
}
$crmService = $resolveTeamCrmConnection->resolveForTeam($crmConfig->getTeam());
$logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities', [
'team' => $crmConfig->getTeam()->getId(),
'parameters' => $this->parameters,
'strategy' => $this->strategy,
]);
$crmService->syncOpportunities($this->parameters, $this->strategy);
$durationMs = round((microtime(true) - $startTime) * 1000, 2);
$logger->info('[' . $crmService->getDisplayName() . '] Syncing opportunities finished', [
'team' => $crmConfig->getTeam()->getId(),
'strategy' => $this->strategy,
'duration_ms' => $durationMs,
]);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2754
|
111
|
46
|
2026-05-07T11:39:01.435846+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153941435_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncOpportunitiesJob.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, 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":"master, menu","depth":5,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
iTerm2• 0She Project: faVsco.js, menu
master, menu
iTerm2• 0ShellEditViewSessionDOCKERO 81ScriptsProfilesWindowDEV (docker)H82Help$0l46]Support Daily - in 21 m100% <78DEV (docker)APP (-zsh)-zsh• *4screenpipe"eventsroutesviewsjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: stoppedworker-download:worker-download_00:stoppedjiminny-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:stoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00: stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-emails:worker-emails_00: stoppedworker-es-update:worker-es-update_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00: startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedrootedocker_Lamp_1:/home/Jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'Syncing opportunity for HubspotSyncing opportunities modified since 2026-05-01 00:00:00...Synced 6 opportunities.root@docker_lamp_1:/home/jiminny# ]•$52.35ms DONE1.64ms DONE3.18ms DONE-zshThu 7 May 14:39:01T81₴6DEV...
|
2751
|
NULL
|
NULL
|
NULL
|
|
2757
|
111
|
47
|
2026-05-07T11:39:04.945813+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153944945_m1.jpg...
|
PhpStorm
|
faVsco.js – OpportunitySyncTrait.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
32
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use HubSpot\Client\Crm\Deals\Model\CollectionResponseAssociatedId;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Account;
use Exception;
use Jiminny\Component\DealInsights\Forecast\Forecast;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Exceptions\CrmException;
use Jiminny\Models\Opportunity;
use Illuminate\Support\Collection;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Crm\Hubspot\DealFieldsService;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\HubspotSingleSyncStrategy;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Utils\CurrencyFormatter;
/**
* Optimized sync methods for better performance
* These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains
*/
trait OpportunitySyncTrait
{
private const int BATCH_SIZE = 100;
private const int BATCH_PROCESS_SIZE = 800;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected DealFieldsService $dealFieldsService;
private ?array $cachedClosedDealStages = null;
private array $cachedBusinessProcesses = [];
private array $cachedStages = [];
/** @var array<string, array<string>> keyed by config id */
private array $cachedOpportunitySyncableFields = [];
/** @var array<string, mixed> keyed by configId:ownerId */
private array $cachedOwnerProfiles = [];
/** @var array<string, mixed> keyed by configId:businessProcessId */
private array $cachedRecordTypes = [];
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$startTime = microtime(true);
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$parameters['config'] = $this->config;
$syncCount = 0;
$reportedTotal = 0;
$lastSyncedId = [];
$strategyNames = [];
try {
foreach ($strategies as $strategyName => $syncStrategy) {
$strategyNames[] = $strategyName;
$this->logger->info(
'[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,
['team' => $this->team->getId()]
);
$total = 0;
$lastId = null;
$buffer = [];
// HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies
foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {
$buffer[] = $hsOpportunity;
// process every 800 rows (fits < 1 000 association limit)
if (\count($buffer) >= self::BATCH_PROCESS_SIZE) {
$syncCount += $this->processOpportunityBatch($buffer);
$buffer = [];
}
}
// leftovers
if ($buffer) {
$syncCount += $this->processOpportunityBatch($buffer);
}
$reportedTotal += $total;
$lastSyncedId = $lastId;
}
} catch (\HubSpot\Client\Crm\Deals\ApiException | CrmException $e) {
$this->handleSyncException($e, $parameters);
}
$durationMs = round((microtime(true) - $startTime) * 1000, 2);
$this->logger->info(
'[HubSpot] Synced opportunities',
[
'team' => $this->team->getId(),
'strategies' => implode(',', $strategyNames),
'sync_count' => $syncCount,
'total' => $reportedTotal,
'last_synced_id' => $lastSyncedId,
'duration_ms' => $durationMs,
]
);
return $reportedTotal;
}
private function handleSyncException(\Throwable $e, array $parameters): void
{
if (($parameters['since'] ?? null) instanceof Carbon) {
$parameters['since'] = $parameters['since']->toDateTimeString();
}
$parameters['config'] = $this->config->getId();
$this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [
'teamId' => $this->team->getUuid(),
'parameters' => $parameters,
'reason' => $e->getMessage(),
]);
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = [
'config' => $this->config,
'crm_id' => $crmId,
];
try {
if (! $strategy instanceof HubspotSingleSyncStrategy) {
throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');
}
$hsOpportunity = $strategy->fetchOpportunity($parameters);
} catch (\HubSpot\Client\Crm\Deals\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
$hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);
return $this->importOrUpdateOpportunity($hsOpportunity);
}
/**
* Process webhook-collected opportunity batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportOpportunityBatch jobs for batch processing.
*
* @return int Number of opportunity IDs dispatched to jobs
*/
public function batchSyncOpportunities(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
}
/**
* Import a batch of opportunities by their CRM IDs.
* Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().
*
* @param array<string> $crmIds HubSpot deal CRM IDs
*
* @return array{success: array, failed_ids: array, errors?: array<string, string>}
*/
public function importOpportunityBatchByIds(array $crmIds): array
{
$fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);
$allDeals = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
$deals = $this->client->getOpportunitiesByIds($chunk, $fields);
foreach ($deals as $deal) {
$allDeals[] = $deal;
}
}
// IDs not returned by HubSpot are likely deleted or inaccessible deals.
// These are not failures — retrying won't bring them back.
$fetchedIds = array_map('strval', array_column($allDeals, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allDeals),
]);
}
if (empty($allDeals)) {
return ['success' => [], 'failed_ids' => []];
}
return $this->importOpportunityBatch($allDeals);
}
private function getClosedDealStages(): array
{
if ($this->cachedClosedDealStages !== null) {
return $this->cachedClosedDealStages;
}
$stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);
$data = [
'lost' => [],
'won' => [],
];
foreach ($stages as $stage) {
if ($stage->probability == 0.00) {
$data['lost'][] = $stage->crm_provider_id;
}
if ($stage->probability == 100.00) {
$data['won'][] = $stage->crm_provider_id;
}
}
$this->cachedClosedDealStages = $data;
return $data;
}
/**
* Import deals into the database with pre-fetched associations.
*
* API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT
* caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()
* where Laravel retries the whole job with backoff. After all retries exhausted,
* failed() requeues all IDs to Redis.
*
* The per-deal loop catches exceptions individually. A deal can end up in three states:
* - success: imported/updated successfully
* - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)
* These are permanent issues — retrying won't fix them.
* - skipped (null): missing dependencies (no account, unknown pipeline/stage).
* This is acceptable — the deal cannot be imported until those exist.
*/
private function importOpportunityBatch(array $deals): array
{
$syncedOpportunities = [
'success' => [],
'failed_ids' => [],
];
$dealIds = array_column($deals, 'id');
$batchStart = microtime(true);
$slowDeals = [];
// Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the
// queue job retries the whole batch and eventually requeues all deal IDs back to Redis.
try {
$companyAssocStart = microtime(true);
$companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');
$companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);
$contactAssocStart = microtime(true);
$contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');
$contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);
$prepareStart = microtime(true);
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
$prepareTimings = [];
$associationsData = $this->prepareAssociatedEntities(
$companyAssociations,
$contactAssociations,
$prepareTimings
);
$prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);
$missingCompanies = count(array_diff(
$allCompanyIds,
array_keys($associationsData['company_id_mappings'] ?? [])
));
$missingContacts = count(array_diff(
$allContactIds,
array_keys($associationsData['contact_id_mappings'] ?? [])
));
$existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(
$this->config,
array_map('strval', $dealIds)
);
$existingCrmIdSet = array_flip($existingCrmIds);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [
'teamId' => $this->team->getId(),
'dealCount' => count($dealIds),
'error' => $e->getMessage(),
]);
throw $e;
}
$loopStart = microtime(true);
foreach ($deals as $deal) {
$dealStart = microtime(true);
try {
$deal['associations'] = $this->prepareAssociationsForOpportunity(
$deal['id'],
$companyAssociations,
$contactAssociations,
$associationsData
);
$syncedOpportunity = $this->importOrUpdateOpportunity(
$deal,
isset($existingCrmIdSet[(string) $deal['id']])
);
if ($syncedOpportunity) {
$syncedOpportunities['success'][] = $syncedOpportunity;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [
'teamId' => $this->team->getId(),
'crmId' => $deal['id'],
'error' => $e->getMessage(),
]);
$syncedOpportunities['failed_ids'][] = $deal['id'];
$syncedOpportunities['errors'][$deal['id']] = $e->getMessage();
}
$dealMs = (int) round((microtime(true) - $dealStart) * 1000);
if ($dealMs > 1000) {
$slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [
'teamId' => $this->team->getId(),
'deal_count' => count($deals),
'total_ms' => $totalMs,
'company_assoc_api_ms' => $companyAssocMs,
'contact_assoc_api_ms' => $contactAssocMs,
'prepare_entities_ms' => $prepareMs,
'prepare_accounts_ms' => $prepareTimings['accounts_ms'],
'prepare_contacts_ms' => $prepareTimings['contacts_ms'],
'total_companies' => count($allCompanyIds),
'missing_companies' => $missingCompanies,
'total_contacts' => count($allContactIds),
'missing_contacts' => $missingContacts,
'deals_loop_ms' => $loopMs,
'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,
'slow_deals_count' => count($slowDeals),
'slow_deals' => array_slice($slowDeals, 0, 10),
]);
return $syncedOpportunities;
}
/**
* Prepare associated entities for opportunities with optimized batch processing
* Returns structured data with CRM ID to DB ID mappings for each opportunity
*/
private function prepareAssociatedEntities(
array $companyAssociations,
array $contactAssociations,
array &$timings = []
): array {
// Step 1: Collect all unique company and contact IDs from associations
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
// Step 2: Batch sync missing entities and get CRM ID to DB ID mappings
$companyIdMappings = [];
$contactIdMappings = [];
$accountsMs = 0;
$contactsMs = 0;
if (! empty($allCompanyIds)) {
$start = microtime(true);
$companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);
$accountsMs = (int) round((microtime(true) - $start) * 1000);
}
if (! empty($allContactIds)) {
$start = microtime(true);
$contactIdMappings = $this->prepareAssociatedContacts($allContactIds);
$contactsMs = (int) round((microtime(true) - $start) * 1000);
}
$timings = [
'accounts_ms' => $accountsMs,
'contacts_ms' => $contactsMs,
];
return [
'company_id_mappings' => $companyIdMappings,
'contact_id_mappings' => $contactIdMappings,
];
}
/**
* Flatten association data to get unique IDs
*/
private function flattenAssociationIds(array $associations): array
{
$ids = [];
foreach ($associations as $dealAssociations) {
if (is_array($dealAssociations)) {
foreach ($dealAssociations as $id) {
$ids[$id] = true;
}
}
}
return array_keys($ids);
}
/**
* Batch sync missing accounts
*/
private function prepareAssociatedAccounts(array $companyIds): array
{
// Find which accounts already exist (lean covering-index lookup)
$existingAccountsData = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));
if (empty($missingCompanyIds)) {
return $existingAccountsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [
'teamId' => $this->team->getUuid(),
'total_companies' => count($companyIds),
'existing_companies' => count($existingAccountsData),
'missing_companies' => count($missingCompanyIds),
]);
// we already have limit on opportunity ids count
// Initialize variable before try block
$syncedAccountsData = [];
try {
$syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [
'size' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
$syncedAccountsData = [];
}
return $existingAccountsData + $syncedAccountsData;
}
/**
* Prepare associated contacts - find existing and sync missing ones
* Returns mapping of CRM ID to DB ID
*/
private function prepareAssociatedContacts(array $contactIds): array
{
// Find which contacts already exist (lean covering-index lookup)
$existingContactsData = $this->crmEntityRepository
->getExistingContactIdsMap($this->config, $contactIds);
$missingContactIds = array_diff($contactIds, array_keys($existingContactsData));
if (empty($missingContactIds)) {
return $existingContactsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [
'teamId' => $this->team->getUuid(),
'total_contacts' => count($contactIds),
'existing_contacts' => count($existingContactsData),
'missing_contacts' => count($missingContactIds),
]);
// Sync missing contacts using batch API
try {
$syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [
'size' => count($missingContactIds),
'error' => $e->getMessage(),
]);
$syncedContactsData = [];
}
return $existingContactsData + $syncedContactsData;
}
private function batchSyncCrmObjects(string $objectType, array $crmIds): array
{
$syncObjects = [];
$crmObjectIds = array_values($crmIds);
foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {
try {
$objects = $objectType === 'companies' ?
$this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :
$this->client->getContactsByIds($chunk, $this->getContactFields());
foreach ($objects as $objectId => $objectData) {
$this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [
'requested_count' => count($chunk),
'synced_count' => count($objects),
]);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [
'ids' => $chunk,
'error' => $e->getMessage(),
]);
}
}
return $syncObjects;
}
private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void
{
try {
$object = $objectType === 'companies' ?
$this->importAccount($objectData) :
$this->importContact($objectData);
if ($object) {
$syncObjects[$object->getCrmProviderId()] = $object->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [
'id' => $objectId,
'error' => $e->getMessage(),
]);
}
}
/**
* Prepare associations for a single opportunity
*
* The return value is an array with the following structure:
* [
* 'companies' => [
* $companyCrmId => $companyId,
* ...
* ],
* 'contacts' => [
* $contactCrmId => $contactId,
* ...
* ],
* 'account_id' => $accountId,
* ]
*/
private function prepareAssociationsForOpportunity(
string $oppCrmId,
array $companyAssociations,
array $contactAssociations,
array $associationsData
): array {
$associations = [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
$oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];
foreach ($oppCompanyIds as $companyCrmId) {
if (isset($associationsData['company_id_mappings'][$companyCrmId])) {
$associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];
// Set primary account (first company becomes primary account)
if ($associations['account_id'] === null) {
$associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];
}
}
}
$oppContactIds = $contactAssociations[$oppCrmId] ?? [];
foreach ($oppContactIds as $contactCrmId) {
if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {
$associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];
}
}
return $associations;
}
/**
* Update only associations for an opportunity
*/
private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void
{
// Update contact associations
$this->importOpportunityContacts($opportunity, $associations['contacts']);
// Update company (account) associations
$this->updateOpportunityAccount($opportunity, $associations['account_id']);
}
/**
* Remove all contact associations from an opportunity
*/
private function removeAllOpportunityContacts(Opportunity $opportunity): void
{
$currentCount = (int) $opportunity->contacts()->count();
if ($currentCount > 0) {
$opportunity->contacts()->detach();
$this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_count' => $currentCount,
]);
}
}
private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void
{
if ($accountId === null) {
// No account ID provided - keep current account
return;
}
$currentAccountId = $opportunity->getAccountId();
// Only update if account has changed
if ($currentAccountId !== $accountId) {
$opportunity->account_id = $accountId;
$opportunity->save();
$this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [
'opportunity_id' => $opportunity->getId(),
'old_account_id' => $currentAccountId,
'new_account_id' => $accountId,
]);
}
}
/**
* Find existing opportunities by external IDs (OPTIMIZED VERSION)
* Uses batch query for better performance
*/
private function findExistingOpportunities(array $crmIds): Collection
{
return $this->crmEntityRepository
->findOpportunitiesByExternalIds($this->config, $crmIds);
}
private function processOpportunityBatch(array $opportunities): int
{
$syncedOpportunities = $this->importOpportunityBatch($opportunities);
return count($syncedOpportunities['success'] ?? []);
}
/**
* Convert single deal associations from HubSpot format to internal format
* Handles both HubSpot SDK objects and array formats
*
* @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed
*
* @return array Processed associations with DB IDs
*/
private function convertDealAssociations(array $opportunityAssociations): array
{
$associations = $this->initializeAssociationsStructure();
if (empty($opportunityAssociations)) {
return $associations;
}
$associationIds = $this->extractAssociationIds($opportunityAssociations);
$this->processCompanyAssociations($associationIds, $associations);
$this->processContactAssociations($associationIds, $associations);
return $associations;
}
private function initializeAssociationsStructure(): array
{
return [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
}
private function extractAssociationIds(array $opportunityAssociations): array
{
$associationIds = [];
foreach ($opportunityAssociations as $type => $associationData) {
if (! empty($associationData)) {
$associationIds[$type] = $this->convertSingleDealAssociations($associationData);
}
}
return $associationIds;
}
private function processCompanyAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['companies'])) {
return;
}
$companyId = $associationIds['companies'][0];
$account = $this->findOrSyncAccount($companyId);
if ($account instanceof Account) {
$associations['companies'][$companyId] = $account->getId();
$associations['account_id'] = $account->getId();
}
}
private function processContactAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['contacts'])) {
return;
}
foreach ($associationIds['contacts'] as $contactId) {
$contact = $this->findOrSyncContact($contactId);
if ($contact instanceof Contact) {
$associations['contacts'][$contactId] = $contact->getId();
}
}
}
private function findOrSyncAccount(string $companyId): ?Account
{
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);
if (! $account instanceof Account) {
$account = $this->syncAccount($companyId);
}
return $account;
}
private function findOrSyncContact(string $contactId): ?Contact
{
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);
if (! $contact instanceof Contact) {
$contact = $this->syncContact($contactId);
}
return $contact;
}
private function convertSingleDealAssociations($opportunityAssociations = null): array
{
$associationData = [];
if ($opportunityAssociations === null) {
return $associationData;
}
// Handle array input (from extractAssociationIds)
if (is_array($opportunityAssociations)) {
return $opportunityAssociations;
}
// Handle CollectionResponseAssociatedId object
if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {
foreach ($opportunityAssociations->getResults() as $association) {
$associationData[] = $association->getId();
}
}
return $associationData;
}
private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity
{
if (empty($crmData['properties'])) {
return null;
}
$crmId = (string) $crmData['id'];
$properties = $crmData['properties'];
$associations = $crmData['associations'] ?? [];
$opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(
$this->config,
$crmId
);
if ($opportunityExists) {
return $this->updateOpportunity($crmId, $properties, $associations);
}
return $this->createOpportunity($crmId, $properties, $associations);
}
/**
* Create new opportunity
*/
private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity
{
$accountId = $this->resolveAccountId($associations);
if (! $accountId) {
return null;
}
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
if (! $businessProcess) {
return null;
}
$stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);
if (! $stage) {
return null;
}
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->importOpportunityContacts($opportunity, $associations['contacts']);
if ($opportunity->wasRecentlyCreated) {
MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());
}
return $opportunity;
}
/**
* Update existing opportunity
*/
private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity
{
$accountId = $this->resolveAccountId($associations);
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
$stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->updateOpportunityAssociations($opportunity, $associations);
return $opportunity;
}
private function resolveAccountId(array $associations): ?int
{
if (! empty($associations['account_id'])) {
return $associations['account_id'];
}
if (empty($associations)) {
return null;
}
// Fallback: use first company as account (currently SDK returns one company)
foreach ($associations['companies'] as $accountId) {
return $accountId;
}
return null;
}
private function buildOpportunityData(
array $properties,
?int $accountId,
?BusinessProcess $businessProcess,
?Stage $stage
): array {
$ownerId = null;
$profile = null;
if (! empty($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->getCachedOwnerProfile((string) $ownerId);
}
$name = 'Unknown';
if (isset($properties['dealname'])) {
$name = mb_strimwidth($properties['dealname'], 0, 128);
}
$amount = $this->resolveAmount($properties);
$currency = $properties['deal_currency_code'] ?? null;
$closeDate = null;
if (! empty($properties['closedate'])) {
$closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');
}
$remotelyCreatedAt = null;
if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {
$date = $this->parseCleanDatetime($properties['createdate']);
$remotelyCreatedAt = $date?->format('Y-m-d H:i:s');
}
$closedStages = $this->getClosedDealStages();
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$data = [
'team_id' => $this->team->getId(),
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => $name,
'value' => ! empty($amount) ? $amount : null,
'currency_code' => CurrencyFormatter::formatCode($currency),
'close_date' => $closeDate,
'is_closed' => $isWon || $isLost,
'is_won' => $isWon,
'remotely_created_at' => $remotelyCreatedAt,
'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),
'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),
];
if ($accountId) {
$data['account_id'] = $accountId;
}
if ($stage) {
$data['stage_id'] = $stage->id;
}
if ($businessProcess) {
$recordType = $this->getCachedBusinessProcessRecordType($businessProcess);
if ($recordType) {
$data['record_type_id'] = $recordType->id;
}
}
return $data;
}
private function getCachedOwnerProfile(string $ownerId): mixed
{
$cacheKey = $this->config->getId() . ':' . $ownerId;
if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {
return $this->cachedOwnerProfiles[$cacheKey];
}
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);
$this->cachedOwnerProfiles[$cacheKey] = $profile;
return $profile;
}
private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed
{
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId();
if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {
return $this->cachedRecordTypes[$cacheKey];
}
$recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);
$this->cachedRecordTypes[$cacheKey] = $recordType;
return $recordType;
}
private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess
{
if ($pipelineId === null) {
return null;
}
$cacheKey = $this->getBusinessProcessCacheKey($pipelineId);
if (isset($this->cachedBusinessProcesses[$cacheKey])) {
return $this->cachedBusinessProcesses[$cacheKey];
}
$businessProcess = $this->getBusinessProcess($pipelineId);
if (! $businessProcess instanceof BusinessProcess) {
$this->importStages();
$businessProcess = $this->getBusinessProcess($pipelineId);
}
if (! $businessProcess instanceof BusinessProcess) {
$this->logger->info(
'[HubSpot] Deal is not attached to a pipeline',
[
'pipeline' => $pipelineId]
);
}
$this->cachedBusinessProcesses[$cacheKey] = $businessProcess;
return $businessProcess;
}
private function getBusinessProcess(string $pipelineId): ?BusinessProcess
{
return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);
}
private function getBusinessProcessCacheKey(string $pipelineId): string
{
return $this->config->getId() . '_' . $pipelineId;
}
private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage
{
if (empty($stageId)) {
return null;
}
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;
if (isset($this->cachedStages[$cacheKey])) {
return $this->cachedStages[$cacheKey];
}
$stage = $this->crmEntityRepository->getPipelineStageByConditions(
$businessProcess,
[
'crm_provider_id' => $stageId,
'type' => Stage::TYPE_OPPORTUNITY,
]
);
if ($stage === null) {
$this->importStages(null, $stageId);
}
if ($stage === null) {
$this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);
}
$this->cachedStages[$cacheKey] = $stage;
return $stage;
}
private function resolveAmount(array $properties): ?string
{
$amount = null;
if (! empty($properties['amount'])) {
$amount = str_replace(',', '', $properties['amount']);
}
if ($this->config->hasDefaultCurrencyFieldSet()) {
$valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();
$amount = $properties[$valueFieldName] ?? $amount;
}
return $amount;
}
private function parseCleanDatetime(string $datetime): ?Carbon
{
// Treat pre-1980 values as invalid
$minValidDate = Carbon::parse('1980-01-01 00:00:00');
try {
$date = Carbon::parse($datetime);
if ($minValidDate->gt($date)) {
return null;
}
return $date;
} catch (Exception) {
return null; // On parse error, treat as null
}
}
private function resolveDealProbability(?string $stageProbability): int
{
if ($stageProbability === null) {
return 0;
}
$probability = (float) $stageProbability;
return $probability > 1 ? 0 : (int) ($probability * 100);
}
private function resolveForecastCategory(?string $forecastCategory): string
{
if (! $forecastCategory) {
return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;
}
$forecastCategory = str_replace('_', ' ', $forecastCategory);
return ucwords(strtolower($forecastCategory));
}
private function importExternalFieldData(array $properties, int $opportunityId): void
{
$this->importOpportunityCrmFieldData(
$properties,
$this->getCachedOpportunitySyncableFields(),
$opportunityId
);
}
private function getCachedOpportunitySyncableFields(): array
{
$cacheKey = (string) $this->config->getId();
if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {
$this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();
}
return $this->cachedOpportunitySyncableFields[$cacheKey];
}
private function importOpportunityContacts(Opportunity $opportunity, array $associations): void
{
// Handle empty or missing contact associations
if (empty($associations)) {
// Remove all existing contact associations if none provided
$this->removeAllOpportunityContacts($opportunity);
return;
}
// Use differential sync approach for better performance and accuracy
$this->syncOpportunityContactsDifferential($opportunity, $associations);
}
/**
* Sync opportunity contacts using differential approach
* This compares current vs new associations and only makes necessary changes
*/
private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void
{
$currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);
$contactAssociationIds = array_keys($contactAssociations);
$contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);
$contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);
if (empty($contactsToAdd) && empty($contactsToRemove)) {
return;
}
$this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);
$this->removeContactAssociations($opportunity, $contactsToRemove);
$this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);
}
private function getCurrentContactCrmIds(Opportunity $opportunity): array
{
return $opportunity->contacts()
->pluck('contacts.crm_provider_id')
->toArray();
}
private function logContactAssociationChanges(
Opportunity $opportunity,
array $currentContactCrmIds,
array $contactAssociations,
array $contactsToAdd,
array $contactsToRemove
): void {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [
'opportunity_id' => $opportunity->getId(),
'current_contacts' => $currentContactCrmIds,
'new_contacts' => $contactAssociations,
'contacts_to_add' => $contactsToAdd,
'contacts_to_remove' => $contactsToRemove,
]);
}
private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void
{
if (empty($contactsToRemove)) {
return;
}
$contactsToDetach = $opportunity->contacts()
->whereIn('contacts.crm_provider_id', $contactsToRemove)
->pluck('contacts.id')
->toArray();
if (! empty($contactsToDetach)) {
$opportunity->contacts()->detach($contactsToDetach);
$this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_contact_crm_ids' => $contactsToRemove,
'removed_contact_count' => count($contactsToDetach),
]);
}
}
private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void
{
if (empty($contactsToAdd)) {
return;
}
$contactsAdded = [];
foreach ($contactsToAdd as $crmId) {
$id = $contactAssociations[$crmId];
if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {
$contactsAdded[] = $crmId;
}
}
$this->logAddedContacts($opportunity, $contactsAdded);
}
private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool
{
try {
return $this->performContactAttachment($opportunity, $id, $crmId);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [
'opportunity_id' => $opportunity->getId(),
'contact_crm_id' => $crmId,
'error' => $e->getMessage(),
]);
return false;
}
}
private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool
{
try {
$opportunity->contacts()->attach($contactId, [
'crm_provider_id' => $crmId,
]);
return true;
} catch (\Illuminate\Database\QueryException $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [
'contact_id' => $contactId,
'contact_crm_id' => $crmId,
'opportunity_id' => $opportunity->getId(),
]);
return false;
}
throw $e;
}
}
private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void
{
if (! empty($contactsAdded)) {
$this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [
'opportunity_id' => $opportunity->getId(),
'added_contact_crm_ids' => $contactsAdded,
'added_contacts_count' => count($contactsAdded),
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"32","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","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\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseAssociatedId;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Account;\nuse Exception;\nuse Jiminny\\Component\\DealInsights\\Forecast\\Forecast;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Models\\Opportunity;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DealFieldsService;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\HubspotSingleSyncStrategy;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Utils\\CurrencyFormatter;\n\n/**\n * Optimized sync methods for better performance\n * These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains\n */\ntrait OpportunitySyncTrait\n{\n private const int BATCH_SIZE = 100;\n private const int BATCH_PROCESS_SIZE = 800;\n\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected DealFieldsService $dealFieldsService;\n\n private ?array $cachedClosedDealStages = null;\n private array $cachedBusinessProcesses = [];\n private array $cachedStages = [];\n /** @var array<string, array<string>> keyed by config id */\n private array $cachedOpportunitySyncableFields = [];\n /** @var array<string, mixed> keyed by configId:ownerId */\n private array $cachedOwnerProfiles = [];\n /** @var array<string, mixed> keyed by configId:businessProcessId */\n private array $cachedRecordTypes = [];\n\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $startTime = microtime(true);\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n $parameters['config'] = $this->config;\n $syncCount = 0;\n $reportedTotal = 0;\n $lastSyncedId = [];\n $strategyNames = [];\n\n try {\n foreach ($strategies as $strategyName => $syncStrategy) {\n $strategyNames[] = $strategyName;\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,\n ['team' => $this->team->getId()]\n );\n\n $total = 0;\n $lastId = null;\n $buffer = [];\n\n // HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies\n foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {\n $buffer[] = $hsOpportunity;\n\n // process every 800 rows (fits < 1 000 association limit)\n if (\\count($buffer) >= self::BATCH_PROCESS_SIZE) {\n $syncCount += $this->processOpportunityBatch($buffer);\n $buffer = [];\n }\n }\n\n // leftovers\n if ($buffer) {\n $syncCount += $this->processOpportunityBatch($buffer);\n }\n\n $reportedTotal += $total;\n $lastSyncedId = $lastId;\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException | CrmException $e) {\n $this->handleSyncException($e, $parameters);\n }\n\n $durationMs = round((microtime(true) - $startTime) * 1000, 2);\n $this->logger->info(\n '[HubSpot] Synced opportunities',\n [\n 'team' => $this->team->getId(),\n 'strategies' => implode(',', $strategyNames),\n 'sync_count' => $syncCount,\n 'total' => $reportedTotal,\n 'last_synced_id' => $lastSyncedId,\n 'duration_ms' => $durationMs,\n ]\n );\n\n return $reportedTotal;\n }\n\n private function handleSyncException(\\Throwable $e, array $parameters): void\n {\n if (($parameters['since'] ?? null) instanceof Carbon) {\n $parameters['since'] = $parameters['since']->toDateTimeString();\n }\n $parameters['config'] = $this->config->getId();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [\n 'teamId' => $this->team->getUuid(),\n 'parameters' => $parameters,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = [\n 'config' => $this->config,\n 'crm_id' => $crmId,\n ];\n\n try {\n if (! $strategy instanceof HubspotSingleSyncStrategy) {\n throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');\n }\n\n $hsOpportunity = $strategy->fetchOpportunity($parameters);\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);\n\n return $this->importOrUpdateOpportunity($hsOpportunity);\n }\n\n /**\n * Process webhook-collected opportunity batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportOpportunityBatch jobs for batch processing.\n *\n * @return int Number of opportunity IDs dispatched to jobs\n */\n public function batchSyncOpportunities(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n }\n\n /**\n * Import a batch of opportunities by their CRM IDs.\n * Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().\n *\n * @param array<string> $crmIds HubSpot deal CRM IDs\n *\n * @return array{success: array, failed_ids: array, errors?: array<string, string>}\n */\n public function importOpportunityBatchByIds(array $crmIds): array\n {\n $fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);\n\n $allDeals = [];\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n $deals = $this->client->getOpportunitiesByIds($chunk, $fields);\n foreach ($deals as $deal) {\n $allDeals[] = $deal;\n }\n }\n\n // IDs not returned by HubSpot are likely deleted or inaccessible deals.\n // These are not failures — retrying won't bring them back.\n $fetchedIds = array_map('strval', array_column($allDeals, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allDeals),\n ]);\n }\n\n if (empty($allDeals)) {\n return ['success' => [], 'failed_ids' => []];\n }\n\n return $this->importOpportunityBatch($allDeals);\n }\n\n private function getClosedDealStages(): array\n {\n if ($this->cachedClosedDealStages !== null) {\n return $this->cachedClosedDealStages;\n }\n\n $stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);\n $data = [\n 'lost' => [],\n 'won' => [],\n ];\n\n foreach ($stages as $stage) {\n if ($stage->probability == 0.00) {\n $data['lost'][] = $stage->crm_provider_id;\n }\n if ($stage->probability == 100.00) {\n $data['won'][] = $stage->crm_provider_id;\n }\n }\n\n $this->cachedClosedDealStages = $data;\n\n return $data;\n }\n\n /**\n * Import deals into the database with pre-fetched associations.\n *\n * API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT\n * caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()\n * where Laravel retries the whole job with backoff. After all retries exhausted,\n * failed() requeues all IDs to Redis.\n *\n * The per-deal loop catches exceptions individually. A deal can end up in three states:\n * - success: imported/updated successfully\n * - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)\n * These are permanent issues — retrying won't fix them.\n * - skipped (null): missing dependencies (no account, unknown pipeline/stage).\n * This is acceptable — the deal cannot be imported until those exist.\n */\n private function importOpportunityBatch(array $deals): array\n {\n $syncedOpportunities = [\n 'success' => [],\n 'failed_ids' => [],\n ];\n $dealIds = array_column($deals, 'id');\n $batchStart = microtime(true);\n $slowDeals = [];\n\n // Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the\n // queue job retries the whole batch and eventually requeues all deal IDs back to Redis.\n try {\n $companyAssocStart = microtime(true);\n $companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');\n $companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);\n\n $contactAssocStart = microtime(true);\n $contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');\n $contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);\n\n $prepareStart = microtime(true);\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n $prepareTimings = [];\n $associationsData = $this->prepareAssociatedEntities(\n $companyAssociations,\n $contactAssociations,\n $prepareTimings\n );\n $prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $missingCompanies = count(array_diff(\n $allCompanyIds,\n array_keys($associationsData['company_id_mappings'] ?? [])\n ));\n $missingContacts = count(array_diff(\n $allContactIds,\n array_keys($associationsData['contact_id_mappings'] ?? [])\n ));\n\n $existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(\n $this->config,\n array_map('strval', $dealIds)\n );\n $existingCrmIdSet = array_flip($existingCrmIds);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [\n 'teamId' => $this->team->getId(),\n 'dealCount' => count($dealIds),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n $loopStart = microtime(true);\n foreach ($deals as $deal) {\n $dealStart = microtime(true);\n\n try {\n $deal['associations'] = $this->prepareAssociationsForOpportunity(\n $deal['id'],\n $companyAssociations,\n $contactAssociations,\n $associationsData\n );\n\n $syncedOpportunity = $this->importOrUpdateOpportunity(\n $deal,\n isset($existingCrmIdSet[(string) $deal['id']])\n );\n if ($syncedOpportunity) {\n $syncedOpportunities['success'][] = $syncedOpportunity;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $deal['id'],\n 'error' => $e->getMessage(),\n ]);\n $syncedOpportunities['failed_ids'][] = $deal['id'];\n $syncedOpportunities['errors'][$deal['id']] = $e->getMessage();\n }\n\n $dealMs = (int) round((microtime(true) - $dealStart) * 1000);\n if ($dealMs > 1000) {\n $slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [\n 'teamId' => $this->team->getId(),\n 'deal_count' => count($deals),\n 'total_ms' => $totalMs,\n 'company_assoc_api_ms' => $companyAssocMs,\n 'contact_assoc_api_ms' => $contactAssocMs,\n 'prepare_entities_ms' => $prepareMs,\n 'prepare_accounts_ms' => $prepareTimings['accounts_ms'],\n 'prepare_contacts_ms' => $prepareTimings['contacts_ms'],\n 'total_companies' => count($allCompanyIds),\n 'missing_companies' => $missingCompanies,\n 'total_contacts' => count($allContactIds),\n 'missing_contacts' => $missingContacts,\n 'deals_loop_ms' => $loopMs,\n 'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,\n 'slow_deals_count' => count($slowDeals),\n 'slow_deals' => array_slice($slowDeals, 0, 10),\n ]);\n\n return $syncedOpportunities;\n }\n\n /**\n * Prepare associated entities for opportunities with optimized batch processing\n * Returns structured data with CRM ID to DB ID mappings for each opportunity\n */\n private function prepareAssociatedEntities(\n array $companyAssociations,\n array $contactAssociations,\n array &$timings = []\n ): array {\n // Step 1: Collect all unique company and contact IDs from associations\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n\n // Step 2: Batch sync missing entities and get CRM ID to DB ID mappings\n $companyIdMappings = [];\n $contactIdMappings = [];\n $accountsMs = 0;\n $contactsMs = 0;\n\n if (! empty($allCompanyIds)) {\n $start = microtime(true);\n $companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);\n $accountsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n if (! empty($allContactIds)) {\n $start = microtime(true);\n $contactIdMappings = $this->prepareAssociatedContacts($allContactIds);\n $contactsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n $timings = [\n 'accounts_ms' => $accountsMs,\n 'contacts_ms' => $contactsMs,\n ];\n\n return [\n 'company_id_mappings' => $companyIdMappings,\n 'contact_id_mappings' => $contactIdMappings,\n ];\n }\n\n /**\n * Flatten association data to get unique IDs\n */\n private function flattenAssociationIds(array $associations): array\n {\n $ids = [];\n foreach ($associations as $dealAssociations) {\n if (is_array($dealAssociations)) {\n foreach ($dealAssociations as $id) {\n $ids[$id] = true;\n }\n }\n }\n\n return array_keys($ids);\n }\n\n /**\n * Batch sync missing accounts\n */\n private function prepareAssociatedAccounts(array $companyIds): array\n {\n // Find which accounts already exist (lean covering-index lookup)\n $existingAccountsData = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));\n\n if (empty($missingCompanyIds)) {\n return $existingAccountsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [\n 'teamId' => $this->team->getUuid(),\n 'total_companies' => count($companyIds),\n 'existing_companies' => count($existingAccountsData),\n 'missing_companies' => count($missingCompanyIds),\n ]);\n\n // we already have limit on opportunity ids count\n // Initialize variable before try block\n $syncedAccountsData = [];\n\n try {\n $syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [\n 'size' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedAccountsData = [];\n }\n\n return $existingAccountsData + $syncedAccountsData;\n }\n\n /**\n * Prepare associated contacts - find existing and sync missing ones\n * Returns mapping of CRM ID to DB ID\n */\n private function prepareAssociatedContacts(array $contactIds): array\n {\n // Find which contacts already exist (lean covering-index lookup)\n $existingContactsData = $this->crmEntityRepository\n ->getExistingContactIdsMap($this->config, $contactIds);\n\n $missingContactIds = array_diff($contactIds, array_keys($existingContactsData));\n\n if (empty($missingContactIds)) {\n return $existingContactsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [\n 'teamId' => $this->team->getUuid(),\n 'total_contacts' => count($contactIds),\n 'existing_contacts' => count($existingContactsData),\n 'missing_contacts' => count($missingContactIds),\n ]);\n\n // Sync missing contacts using batch API\n try {\n $syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [\n 'size' => count($missingContactIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedContactsData = [];\n }\n\n return $existingContactsData + $syncedContactsData;\n }\n\n private function batchSyncCrmObjects(string $objectType, array $crmIds): array\n {\n $syncObjects = [];\n $crmObjectIds = array_values($crmIds);\n\n foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {\n try {\n $objects = $objectType === 'companies' ?\n $this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :\n $this->client->getContactsByIds($chunk, $this->getContactFields());\n\n foreach ($objects as $objectId => $objectData) {\n $this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [\n 'requested_count' => count($chunk),\n 'synced_count' => count($objects),\n ]);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [\n 'ids' => $chunk,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncObjects;\n }\n\n private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void\n {\n try {\n $object = $objectType === 'companies' ?\n $this->importAccount($objectData) :\n $this->importContact($objectData);\n\n if ($object) {\n $syncObjects[$object->getCrmProviderId()] = $object->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [\n 'id' => $objectId,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n /**\n * Prepare associations for a single opportunity\n *\n * The return value is an array with the following structure:\n * [\n * 'companies' => [\n * $companyCrmId => $companyId,\n * ...\n * ],\n * 'contacts' => [\n * $contactCrmId => $contactId,\n * ...\n * ],\n * 'account_id' => $accountId,\n * ]\n */\n private function prepareAssociationsForOpportunity(\n string $oppCrmId,\n array $companyAssociations,\n array $contactAssociations,\n array $associationsData\n ): array {\n $associations = [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n\n $oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];\n foreach ($oppCompanyIds as $companyCrmId) {\n if (isset($associationsData['company_id_mappings'][$companyCrmId])) {\n $associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];\n\n // Set primary account (first company becomes primary account)\n if ($associations['account_id'] === null) {\n $associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];\n }\n }\n }\n\n $oppContactIds = $contactAssociations[$oppCrmId] ?? [];\n foreach ($oppContactIds as $contactCrmId) {\n if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {\n $associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];\n }\n }\n\n return $associations;\n }\n\n /**\n * Update only associations for an opportunity\n */\n private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void\n {\n // Update contact associations\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n // Update company (account) associations\n $this->updateOpportunityAccount($opportunity, $associations['account_id']);\n }\n\n /**\n * Remove all contact associations from an opportunity\n */\n private function removeAllOpportunityContacts(Opportunity $opportunity): void\n {\n $currentCount = (int) $opportunity->contacts()->count();\n\n if ($currentCount > 0) {\n $opportunity->contacts()->detach();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_count' => $currentCount,\n ]);\n }\n }\n\n private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void\n {\n if ($accountId === null) {\n // No account ID provided - keep current account\n return;\n }\n\n $currentAccountId = $opportunity->getAccountId();\n\n // Only update if account has changed\n if ($currentAccountId !== $accountId) {\n $opportunity->account_id = $accountId;\n $opportunity->save();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [\n 'opportunity_id' => $opportunity->getId(),\n 'old_account_id' => $currentAccountId,\n 'new_account_id' => $accountId,\n ]);\n }\n }\n\n /**\n * Find existing opportunities by external IDs (OPTIMIZED VERSION)\n * Uses batch query for better performance\n */\n private function findExistingOpportunities(array $crmIds): Collection\n {\n return $this->crmEntityRepository\n ->findOpportunitiesByExternalIds($this->config, $crmIds);\n }\n\n private function processOpportunityBatch(array $opportunities): int\n {\n $syncedOpportunities = $this->importOpportunityBatch($opportunities);\n\n return count($syncedOpportunities['success'] ?? []);\n }\n\n /**\n * Convert single deal associations from HubSpot format to internal format\n * Handles both HubSpot SDK objects and array formats\n *\n * @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed\n *\n * @return array Processed associations with DB IDs\n */\n private function convertDealAssociations(array $opportunityAssociations): array\n {\n $associations = $this->initializeAssociationsStructure();\n\n if (empty($opportunityAssociations)) {\n return $associations;\n }\n\n $associationIds = $this->extractAssociationIds($opportunityAssociations);\n\n $this->processCompanyAssociations($associationIds, $associations);\n $this->processContactAssociations($associationIds, $associations);\n\n return $associations;\n }\n\n private function initializeAssociationsStructure(): array\n {\n return [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n }\n\n private function extractAssociationIds(array $opportunityAssociations): array\n {\n $associationIds = [];\n\n foreach ($opportunityAssociations as $type => $associationData) {\n if (! empty($associationData)) {\n $associationIds[$type] = $this->convertSingleDealAssociations($associationData);\n }\n }\n\n return $associationIds;\n }\n\n private function processCompanyAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['companies'])) {\n return;\n }\n\n $companyId = $associationIds['companies'][0];\n $account = $this->findOrSyncAccount($companyId);\n\n if ($account instanceof Account) {\n $associations['companies'][$companyId] = $account->getId();\n $associations['account_id'] = $account->getId();\n }\n }\n\n private function processContactAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['contacts'])) {\n return;\n }\n\n foreach ($associationIds['contacts'] as $contactId) {\n $contact = $this->findOrSyncContact($contactId);\n\n if ($contact instanceof Contact) {\n $associations['contacts'][$contactId] = $contact->getId();\n }\n }\n }\n\n private function findOrSyncAccount(string $companyId): ?Account\n {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);\n\n if (! $account instanceof Account) {\n $account = $this->syncAccount($companyId);\n }\n\n return $account;\n }\n\n private function findOrSyncContact(string $contactId): ?Contact\n {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);\n\n if (! $contact instanceof Contact) {\n $contact = $this->syncContact($contactId);\n }\n\n return $contact;\n }\n\n private function convertSingleDealAssociations($opportunityAssociations = null): array\n {\n $associationData = [];\n\n if ($opportunityAssociations === null) {\n return $associationData;\n }\n\n // Handle array input (from extractAssociationIds)\n if (is_array($opportunityAssociations)) {\n return $opportunityAssociations;\n }\n\n // Handle CollectionResponseAssociatedId object\n if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {\n foreach ($opportunityAssociations->getResults() as $association) {\n $associationData[] = $association->getId();\n }\n }\n\n return $associationData;\n }\n\n private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity\n {\n if (empty($crmData['properties'])) {\n return null;\n }\n\n $crmId = (string) $crmData['id'];\n $properties = $crmData['properties'];\n $associations = $crmData['associations'] ?? [];\n\n $opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(\n $this->config,\n $crmId\n );\n\n if ($opportunityExists) {\n return $this->updateOpportunity($crmId, $properties, $associations);\n }\n\n return $this->createOpportunity($crmId, $properties, $associations);\n }\n\n /**\n * Create new opportunity\n */\n private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n if (! $accountId) {\n return null;\n }\n\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n if (! $businessProcess) {\n return null;\n }\n\n $stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);\n if (! $stage) {\n return null;\n }\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n }\n\n /**\n * Update existing opportunity\n */\n private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n $stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->updateOpportunityAssociations($opportunity, $associations);\n\n return $opportunity;\n }\n\n private function resolveAccountId(array $associations): ?int\n {\n if (! empty($associations['account_id'])) {\n return $associations['account_id'];\n }\n\n if (empty($associations)) {\n return null;\n }\n\n // Fallback: use first company as account (currently SDK returns one company)\n foreach ($associations['companies'] as $accountId) {\n return $accountId;\n }\n\n return null;\n }\n\n private function buildOpportunityData(\n array $properties,\n ?int $accountId,\n ?BusinessProcess $businessProcess,\n ?Stage $stage\n ): array {\n $ownerId = null;\n $profile = null;\n if (! empty($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->getCachedOwnerProfile((string) $ownerId);\n }\n\n $name = 'Unknown';\n if (isset($properties['dealname'])) {\n $name = mb_strimwidth($properties['dealname'], 0, 128);\n }\n\n $amount = $this->resolveAmount($properties);\n $currency = $properties['deal_currency_code'] ?? null;\n\n $closeDate = null;\n if (! empty($properties['closedate'])) {\n $closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');\n }\n\n $remotelyCreatedAt = null;\n if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {\n $date = $this->parseCleanDatetime($properties['createdate']);\n $remotelyCreatedAt = $date?->format('Y-m-d H:i:s');\n }\n\n $closedStages = $this->getClosedDealStages();\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $data = [\n 'team_id' => $this->team->getId(),\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'value' => ! empty($amount) ? $amount : null,\n 'currency_code' => CurrencyFormatter::formatCode($currency),\n 'close_date' => $closeDate,\n 'is_closed' => $isWon || $isLost,\n 'is_won' => $isWon,\n 'remotely_created_at' => $remotelyCreatedAt,\n 'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),\n 'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),\n ];\n\n if ($accountId) {\n $data['account_id'] = $accountId;\n }\n\n if ($stage) {\n $data['stage_id'] = $stage->id;\n }\n\n if ($businessProcess) {\n $recordType = $this->getCachedBusinessProcessRecordType($businessProcess);\n if ($recordType) {\n $data['record_type_id'] = $recordType->id;\n }\n }\n\n return $data;\n }\n\n private function getCachedOwnerProfile(string $ownerId): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $ownerId;\n if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {\n return $this->cachedOwnerProfiles[$cacheKey];\n }\n\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);\n $this->cachedOwnerProfiles[$cacheKey] = $profile;\n\n return $profile;\n }\n\n private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId();\n if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {\n return $this->cachedRecordTypes[$cacheKey];\n }\n\n $recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);\n $this->cachedRecordTypes[$cacheKey] = $recordType;\n\n return $recordType;\n }\n\n private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess\n {\n if ($pipelineId === null) {\n return null;\n }\n\n $cacheKey = $this->getBusinessProcessCacheKey($pipelineId);\n if (isset($this->cachedBusinessProcesses[$cacheKey])) {\n return $this->cachedBusinessProcesses[$cacheKey];\n }\n\n $businessProcess = $this->getBusinessProcess($pipelineId);\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->importStages();\n $businessProcess = $this->getBusinessProcess($pipelineId);\n }\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->logger->info(\n '[HubSpot] Deal is not attached to a pipeline',\n [\n 'pipeline' => $pipelineId]\n );\n }\n\n $this->cachedBusinessProcesses[$cacheKey] = $businessProcess;\n\n return $businessProcess;\n }\n\n private function getBusinessProcess(string $pipelineId): ?BusinessProcess\n {\n return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);\n }\n\n private function getBusinessProcessCacheKey(string $pipelineId): string\n {\n return $this->config->getId() . '_' . $pipelineId;\n }\n\n private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage\n {\n if (empty($stageId)) {\n return null;\n }\n\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;\n if (isset($this->cachedStages[$cacheKey])) {\n return $this->cachedStages[$cacheKey];\n }\n\n $stage = $this->crmEntityRepository->getPipelineStageByConditions(\n $businessProcess,\n [\n 'crm_provider_id' => $stageId,\n 'type' => Stage::TYPE_OPPORTUNITY,\n ]\n );\n\n if ($stage === null) {\n $this->importStages(null, $stageId);\n }\n\n if ($stage === null) {\n $this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);\n }\n\n $this->cachedStages[$cacheKey] = $stage;\n\n return $stage;\n }\n\n private function resolveAmount(array $properties): ?string\n {\n $amount = null;\n if (! empty($properties['amount'])) {\n $amount = str_replace(',', '', $properties['amount']);\n }\n\n if ($this->config->hasDefaultCurrencyFieldSet()) {\n $valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();\n $amount = $properties[$valueFieldName] ?? $amount;\n }\n\n return $amount;\n }\n\n private function parseCleanDatetime(string $datetime): ?Carbon\n {\n // Treat pre-1980 values as invalid\n $minValidDate = Carbon::parse('1980-01-01 00:00:00');\n\n try {\n $date = Carbon::parse($datetime);\n\n if ($minValidDate->gt($date)) {\n return null;\n }\n\n return $date;\n } catch (Exception) {\n return null; // On parse error, treat as null\n }\n }\n\n private function resolveDealProbability(?string $stageProbability): int\n {\n if ($stageProbability === null) {\n return 0;\n }\n\n $probability = (float) $stageProbability;\n\n return $probability > 1 ? 0 : (int) ($probability * 100);\n }\n\n private function resolveForecastCategory(?string $forecastCategory): string\n {\n if (! $forecastCategory) {\n return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;\n }\n\n $forecastCategory = str_replace('_', ' ', $forecastCategory);\n\n return ucwords(strtolower($forecastCategory));\n }\n\n private function importExternalFieldData(array $properties, int $opportunityId): void\n {\n $this->importOpportunityCrmFieldData(\n $properties,\n $this->getCachedOpportunitySyncableFields(),\n $opportunityId\n );\n }\n\n private function getCachedOpportunitySyncableFields(): array\n {\n $cacheKey = (string) $this->config->getId();\n if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {\n $this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();\n }\n\n return $this->cachedOpportunitySyncableFields[$cacheKey];\n }\n\n private function importOpportunityContacts(Opportunity $opportunity, array $associations): void\n {\n // Handle empty or missing contact associations\n if (empty($associations)) {\n // Remove all existing contact associations if none provided\n $this->removeAllOpportunityContacts($opportunity);\n\n return;\n }\n\n // Use differential sync approach for better performance and accuracy\n $this->syncOpportunityContactsDifferential($opportunity, $associations);\n }\n\n /**\n * Sync opportunity contacts using differential approach\n * This compares current vs new associations and only makes necessary changes\n */\n private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void\n {\n $currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);\n $contactAssociationIds = array_keys($contactAssociations);\n\n $contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);\n $contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);\n\n if (empty($contactsToAdd) && empty($contactsToRemove)) {\n return;\n }\n\n $this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);\n\n $this->removeContactAssociations($opportunity, $contactsToRemove);\n $this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);\n }\n\n private function getCurrentContactCrmIds(Opportunity $opportunity): array\n {\n return $opportunity->contacts()\n ->pluck('contacts.crm_provider_id')\n ->toArray();\n }\n\n private function logContactAssociationChanges(\n Opportunity $opportunity,\n array $currentContactCrmIds,\n array $contactAssociations,\n array $contactsToAdd,\n array $contactsToRemove\n ): void {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [\n 'opportunity_id' => $opportunity->getId(),\n 'current_contacts' => $currentContactCrmIds,\n 'new_contacts' => $contactAssociations,\n 'contacts_to_add' => $contactsToAdd,\n 'contacts_to_remove' => $contactsToRemove,\n ]);\n }\n\n private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void\n {\n if (empty($contactsToRemove)) {\n return;\n }\n\n $contactsToDetach = $opportunity->contacts()\n ->whereIn('contacts.crm_provider_id', $contactsToRemove)\n ->pluck('contacts.id')\n ->toArray();\n\n if (! empty($contactsToDetach)) {\n $opportunity->contacts()->detach($contactsToDetach);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_contact_crm_ids' => $contactsToRemove,\n 'removed_contact_count' => count($contactsToDetach),\n ]);\n }\n }\n\n private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void\n {\n if (empty($contactsToAdd)) {\n return;\n }\n\n $contactsAdded = [];\n foreach ($contactsToAdd as $crmId) {\n $id = $contactAssociations[$crmId];\n\n if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {\n $contactsAdded[] = $crmId;\n }\n }\n\n $this->logAddedContacts($opportunity, $contactsAdded);\n }\n\n private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool\n {\n try {\n return $this->performContactAttachment($opportunity, $id, $crmId);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [\n 'opportunity_id' => $opportunity->getId(),\n 'contact_crm_id' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool\n {\n try {\n $opportunity->contacts()->attach($contactId, [\n 'crm_provider_id' => $crmId,\n ]);\n\n return true;\n } catch (\\Illuminate\\Database\\QueryException $e) {\n if (str_contains($e->getMessage(), 'Duplicate entry')) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [\n 'contact_id' => $contactId,\n 'contact_crm_id' => $crmId,\n 'opportunity_id' => $opportunity->getId(),\n ]);\n\n return false;\n }\n\n throw $e;\n }\n }\n\n private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void\n {\n if (! empty($contactsAdded)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'added_contact_crm_ids' => $contactsAdded,\n 'added_contacts_count' => count($contactsAdded),\n ]);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseAssociatedId;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Account;\nuse Exception;\nuse Jiminny\\Component\\DealInsights\\Forecast\\Forecast;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Models\\Opportunity;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DealFieldsService;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\HubspotSingleSyncStrategy;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Utils\\CurrencyFormatter;\n\n/**\n * Optimized sync methods for better performance\n * These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains\n */\ntrait OpportunitySyncTrait\n{\n private const int BATCH_SIZE = 100;\n private const int BATCH_PROCESS_SIZE = 800;\n\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected DealFieldsService $dealFieldsService;\n\n private ?array $cachedClosedDealStages = null;\n private array $cachedBusinessProcesses = [];\n private array $cachedStages = [];\n /** @var array<string, array<string>> keyed by config id */\n private array $cachedOpportunitySyncableFields = [];\n /** @var array<string, mixed> keyed by configId:ownerId */\n private array $cachedOwnerProfiles = [];\n /** @var array<string, mixed> keyed by configId:businessProcessId */\n private array $cachedRecordTypes = [];\n\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $startTime = microtime(true);\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n $parameters['config'] = $this->config;\n $syncCount = 0;\n $reportedTotal = 0;\n $lastSyncedId = [];\n $strategyNames = [];\n\n try {\n foreach ($strategies as $strategyName => $syncStrategy) {\n $strategyNames[] = $strategyName;\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,\n ['team' => $this->team->getId()]\n );\n\n $total = 0;\n $lastId = null;\n $buffer = [];\n\n // HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies\n foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {\n $buffer[] = $hsOpportunity;\n\n // process every 800 rows (fits < 1 000 association limit)\n if (\\count($buffer) >= self::BATCH_PROCESS_SIZE) {\n $syncCount += $this->processOpportunityBatch($buffer);\n $buffer = [];\n }\n }\n\n // leftovers\n if ($buffer) {\n $syncCount += $this->processOpportunityBatch($buffer);\n }\n\n $reportedTotal += $total;\n $lastSyncedId = $lastId;\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException | CrmException $e) {\n $this->handleSyncException($e, $parameters);\n }\n\n $durationMs = round((microtime(true) - $startTime) * 1000, 2);\n $this->logger->info(\n '[HubSpot] Synced opportunities',\n [\n 'team' => $this->team->getId(),\n 'strategies' => implode(',', $strategyNames),\n 'sync_count' => $syncCount,\n 'total' => $reportedTotal,\n 'last_synced_id' => $lastSyncedId,\n 'duration_ms' => $durationMs,\n ]\n );\n\n return $reportedTotal;\n }\n\n private function handleSyncException(\\Throwable $e, array $parameters): void\n {\n if (($parameters['since'] ?? null) instanceof Carbon) {\n $parameters['since'] = $parameters['since']->toDateTimeString();\n }\n $parameters['config'] = $this->config->getId();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [\n 'teamId' => $this->team->getUuid(),\n 'parameters' => $parameters,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = [\n 'config' => $this->config,\n 'crm_id' => $crmId,\n ];\n\n try {\n if (! $strategy instanceof HubspotSingleSyncStrategy) {\n throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');\n }\n\n $hsOpportunity = $strategy->fetchOpportunity($parameters);\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);\n\n return $this->importOrUpdateOpportunity($hsOpportunity);\n }\n\n /**\n * Process webhook-collected opportunity batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportOpportunityBatch jobs for batch processing.\n *\n * @return int Number of opportunity IDs dispatched to jobs\n */\n public function batchSyncOpportunities(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n }\n\n /**\n * Import a batch of opportunities by their CRM IDs.\n * Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().\n *\n * @param array<string> $crmIds HubSpot deal CRM IDs\n *\n * @return array{success: array, failed_ids: array, errors?: array<string, string>}\n */\n public function importOpportunityBatchByIds(array $crmIds): array\n {\n $fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);\n\n $allDeals = [];\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n $deals = $this->client->getOpportunitiesByIds($chunk, $fields);\n foreach ($deals as $deal) {\n $allDeals[] = $deal;\n }\n }\n\n // IDs not returned by HubSpot are likely deleted or inaccessible deals.\n // These are not failures — retrying won't bring them back.\n $fetchedIds = array_map('strval', array_column($allDeals, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allDeals),\n ]);\n }\n\n if (empty($allDeals)) {\n return ['success' => [], 'failed_ids' => []];\n }\n\n return $this->importOpportunityBatch($allDeals);\n }\n\n private function getClosedDealStages(): array\n {\n if ($this->cachedClosedDealStages !== null) {\n return $this->cachedClosedDealStages;\n }\n\n $stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);\n $data = [\n 'lost' => [],\n 'won' => [],\n ];\n\n foreach ($stages as $stage) {\n if ($stage->probability == 0.00) {\n $data['lost'][] = $stage->crm_provider_id;\n }\n if ($stage->probability == 100.00) {\n $data['won'][] = $stage->crm_provider_id;\n }\n }\n\n $this->cachedClosedDealStages = $data;\n\n return $data;\n }\n\n /**\n * Import deals into the database with pre-fetched associations.\n *\n * API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT\n * caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()\n * where Laravel retries the whole job with backoff. After all retries exhausted,\n * failed() requeues all IDs to Redis.\n *\n * The per-deal loop catches exceptions individually. A deal can end up in three states:\n * - success: imported/updated successfully\n * - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)\n * These are permanent issues — retrying won't fix them.\n * - skipped (null): missing dependencies (no account, unknown pipeline/stage).\n * This is acceptable — the deal cannot be imported until those exist.\n */\n private function importOpportunityBatch(array $deals): array\n {\n $syncedOpportunities = [\n 'success' => [],\n 'failed_ids' => [],\n ];\n $dealIds = array_column($deals, 'id');\n $batchStart = microtime(true);\n $slowDeals = [];\n\n // Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the\n // queue job retries the whole batch and eventually requeues all deal IDs back to Redis.\n try {\n $companyAssocStart = microtime(true);\n $companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');\n $companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);\n\n $contactAssocStart = microtime(true);\n $contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');\n $contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);\n\n $prepareStart = microtime(true);\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n $prepareTimings = [];\n $associationsData = $this->prepareAssociatedEntities(\n $companyAssociations,\n $contactAssociations,\n $prepareTimings\n );\n $prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $missingCompanies = count(array_diff(\n $allCompanyIds,\n array_keys($associationsData['company_id_mappings'] ?? [])\n ));\n $missingContacts = count(array_diff(\n $allContactIds,\n array_keys($associationsData['contact_id_mappings'] ?? [])\n ));\n\n $existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(\n $this->config,\n array_map('strval', $dealIds)\n );\n $existingCrmIdSet = array_flip($existingCrmIds);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [\n 'teamId' => $this->team->getId(),\n 'dealCount' => count($dealIds),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n $loopStart = microtime(true);\n foreach ($deals as $deal) {\n $dealStart = microtime(true);\n\n try {\n $deal['associations'] = $this->prepareAssociationsForOpportunity(\n $deal['id'],\n $companyAssociations,\n $contactAssociations,\n $associationsData\n );\n\n $syncedOpportunity = $this->importOrUpdateOpportunity(\n $deal,\n isset($existingCrmIdSet[(string) $deal['id']])\n );\n if ($syncedOpportunity) {\n $syncedOpportunities['success'][] = $syncedOpportunity;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $deal['id'],\n 'error' => $e->getMessage(),\n ]);\n $syncedOpportunities['failed_ids'][] = $deal['id'];\n $syncedOpportunities['errors'][$deal['id']] = $e->getMessage();\n }\n\n $dealMs = (int) round((microtime(true) - $dealStart) * 1000);\n if ($dealMs > 1000) {\n $slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [\n 'teamId' => $this->team->getId(),\n 'deal_count' => count($deals),\n 'total_ms' => $totalMs,\n 'company_assoc_api_ms' => $companyAssocMs,\n 'contact_assoc_api_ms' => $contactAssocMs,\n 'prepare_entities_ms' => $prepareMs,\n 'prepare_accounts_ms' => $prepareTimings['accounts_ms'],\n 'prepare_contacts_ms' => $prepareTimings['contacts_ms'],\n 'total_companies' => count($allCompanyIds),\n 'missing_companies' => $missingCompanies,\n 'total_contacts' => count($allContactIds),\n 'missing_contacts' => $missingContacts,\n 'deals_loop_ms' => $loopMs,\n 'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,\n 'slow_deals_count' => count($slowDeals),\n 'slow_deals' => array_slice($slowDeals, 0, 10),\n ]);\n\n return $syncedOpportunities;\n }\n\n /**\n * Prepare associated entities for opportunities with optimized batch processing\n * Returns structured data with CRM ID to DB ID mappings for each opportunity\n */\n private function prepareAssociatedEntities(\n array $companyAssociations,\n array $contactAssociations,\n array &$timings = []\n ): array {\n // Step 1: Collect all unique company and contact IDs from associations\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n\n // Step 2: Batch sync missing entities and get CRM ID to DB ID mappings\n $companyIdMappings = [];\n $contactIdMappings = [];\n $accountsMs = 0;\n $contactsMs = 0;\n\n if (! empty($allCompanyIds)) {\n $start = microtime(true);\n $companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);\n $accountsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n if (! empty($allContactIds)) {\n $start = microtime(true);\n $contactIdMappings = $this->prepareAssociatedContacts($allContactIds);\n $contactsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n $timings = [\n 'accounts_ms' => $accountsMs,\n 'contacts_ms' => $contactsMs,\n ];\n\n return [\n 'company_id_mappings' => $companyIdMappings,\n 'contact_id_mappings' => $contactIdMappings,\n ];\n }\n\n /**\n * Flatten association data to get unique IDs\n */\n private function flattenAssociationIds(array $associations): array\n {\n $ids = [];\n foreach ($associations as $dealAssociations) {\n if (is_array($dealAssociations)) {\n foreach ($dealAssociations as $id) {\n $ids[$id] = true;\n }\n }\n }\n\n return array_keys($ids);\n }\n\n /**\n * Batch sync missing accounts\n */\n private function prepareAssociatedAccounts(array $companyIds): array\n {\n // Find which accounts already exist (lean covering-index lookup)\n $existingAccountsData = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));\n\n if (empty($missingCompanyIds)) {\n return $existingAccountsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [\n 'teamId' => $this->team->getUuid(),\n 'total_companies' => count($companyIds),\n 'existing_companies' => count($existingAccountsData),\n 'missing_companies' => count($missingCompanyIds),\n ]);\n\n // we already have limit on opportunity ids count\n // Initialize variable before try block\n $syncedAccountsData = [];\n\n try {\n $syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [\n 'size' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedAccountsData = [];\n }\n\n return $existingAccountsData + $syncedAccountsData;\n }\n\n /**\n * Prepare associated contacts - find existing and sync missing ones\n * Returns mapping of CRM ID to DB ID\n */\n private function prepareAssociatedContacts(array $contactIds): array\n {\n // Find which contacts already exist (lean covering-index lookup)\n $existingContactsData = $this->crmEntityRepository\n ->getExistingContactIdsMap($this->config, $contactIds);\n\n $missingContactIds = array_diff($contactIds, array_keys($existingContactsData));\n\n if (empty($missingContactIds)) {\n return $existingContactsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [\n 'teamId' => $this->team->getUuid(),\n 'total_contacts' => count($contactIds),\n 'existing_contacts' => count($existingContactsData),\n 'missing_contacts' => count($missingContactIds),\n ]);\n\n // Sync missing contacts using batch API\n try {\n $syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [\n 'size' => count($missingContactIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedContactsData = [];\n }\n\n return $existingContactsData + $syncedContactsData;\n }\n\n private function batchSyncCrmObjects(string $objectType, array $crmIds): array\n {\n $syncObjects = [];\n $crmObjectIds = array_values($crmIds);\n\n foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {\n try {\n $objects = $objectType === 'companies' ?\n $this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :\n $this->client->getContactsByIds($chunk, $this->getContactFields());\n\n foreach ($objects as $objectId => $objectData) {\n $this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [\n 'requested_count' => count($chunk),\n 'synced_count' => count($objects),\n ]);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [\n 'ids' => $chunk,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncObjects;\n }\n\n private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void\n {\n try {\n $object = $objectType === 'companies' ?\n $this->importAccount($objectData) :\n $this->importContact($objectData);\n\n if ($object) {\n $syncObjects[$object->getCrmProviderId()] = $object->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [\n 'id' => $objectId,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n /**\n * Prepare associations for a single opportunity\n *\n * The return value is an array with the following structure:\n * [\n * 'companies' => [\n * $companyCrmId => $companyId,\n * ...\n * ],\n * 'contacts' => [\n * $contactCrmId => $contactId,\n * ...\n * ],\n * 'account_id' => $accountId,\n * ]\n */\n private function prepareAssociationsForOpportunity(\n string $oppCrmId,\n array $companyAssociations,\n array $contactAssociations,\n array $associationsData\n ): array {\n $associations = [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n\n $oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];\n foreach ($oppCompanyIds as $companyCrmId) {\n if (isset($associationsData['company_id_mappings'][$companyCrmId])) {\n $associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];\n\n // Set primary account (first company becomes primary account)\n if ($associations['account_id'] === null) {\n $associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];\n }\n }\n }\n\n $oppContactIds = $contactAssociations[$oppCrmId] ?? [];\n foreach ($oppContactIds as $contactCrmId) {\n if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {\n $associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];\n }\n }\n\n return $associations;\n }\n\n /**\n * Update only associations for an opportunity\n */\n private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void\n {\n // Update contact associations\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n // Update company (account) associations\n $this->updateOpportunityAccount($opportunity, $associations['account_id']);\n }\n\n /**\n * Remove all contact associations from an opportunity\n */\n private function removeAllOpportunityContacts(Opportunity $opportunity): void\n {\n $currentCount = (int) $opportunity->contacts()->count();\n\n if ($currentCount > 0) {\n $opportunity->contacts()->detach();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_count' => $currentCount,\n ]);\n }\n }\n\n private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void\n {\n if ($accountId === null) {\n // No account ID provided - keep current account\n return;\n }\n\n $currentAccountId = $opportunity->getAccountId();\n\n // Only update if account has changed\n if ($currentAccountId !== $accountId) {\n $opportunity->account_id = $accountId;\n $opportunity->save();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [\n 'opportunity_id' => $opportunity->getId(),\n 'old_account_id' => $currentAccountId,\n 'new_account_id' => $accountId,\n ]);\n }\n }\n\n /**\n * Find existing opportunities by external IDs (OPTIMIZED VERSION)\n * Uses batch query for better performance\n */\n private function findExistingOpportunities(array $crmIds): Collection\n {\n return $this->crmEntityRepository\n ->findOpportunitiesByExternalIds($this->config, $crmIds);\n }\n\n private function processOpportunityBatch(array $opportunities): int\n {\n $syncedOpportunities = $this->importOpportunityBatch($opportunities);\n\n return count($syncedOpportunities['success'] ?? []);\n }\n\n /**\n * Convert single deal associations from HubSpot format to internal format\n * Handles both HubSpot SDK objects and array formats\n *\n * @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed\n *\n * @return array Processed associations with DB IDs\n */\n private function convertDealAssociations(array $opportunityAssociations): array\n {\n $associations = $this->initializeAssociationsStructure();\n\n if (empty($opportunityAssociations)) {\n return $associations;\n }\n\n $associationIds = $this->extractAssociationIds($opportunityAssociations);\n\n $this->processCompanyAssociations($associationIds, $associations);\n $this->processContactAssociations($associationIds, $associations);\n\n return $associations;\n }\n\n private function initializeAssociationsStructure(): array\n {\n return [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n }\n\n private function extractAssociationIds(array $opportunityAssociations): array\n {\n $associationIds = [];\n\n foreach ($opportunityAssociations as $type => $associationData) {\n if (! empty($associationData)) {\n $associationIds[$type] = $this->convertSingleDealAssociations($associationData);\n }\n }\n\n return $associationIds;\n }\n\n private function processCompanyAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['companies'])) {\n return;\n }\n\n $companyId = $associationIds['companies'][0];\n $account = $this->findOrSyncAccount($companyId);\n\n if ($account instanceof Account) {\n $associations['companies'][$companyId] = $account->getId();\n $associations['account_id'] = $account->getId();\n }\n }\n\n private function processContactAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['contacts'])) {\n return;\n }\n\n foreach ($associationIds['contacts'] as $contactId) {\n $contact = $this->findOrSyncContact($contactId);\n\n if ($contact instanceof Contact) {\n $associations['contacts'][$contactId] = $contact->getId();\n }\n }\n }\n\n private function findOrSyncAccount(string $companyId): ?Account\n {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);\n\n if (! $account instanceof Account) {\n $account = $this->syncAccount($companyId);\n }\n\n return $account;\n }\n\n private function findOrSyncContact(string $contactId): ?Contact\n {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);\n\n if (! $contact instanceof Contact) {\n $contact = $this->syncContact($contactId);\n }\n\n return $contact;\n }\n\n private function convertSingleDealAssociations($opportunityAssociations = null): array\n {\n $associationData = [];\n\n if ($opportunityAssociations === null) {\n return $associationData;\n }\n\n // Handle array input (from extractAssociationIds)\n if (is_array($opportunityAssociations)) {\n return $opportunityAssociations;\n }\n\n // Handle CollectionResponseAssociatedId object\n if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {\n foreach ($opportunityAssociations->getResults() as $association) {\n $associationData[] = $association->getId();\n }\n }\n\n return $associationData;\n }\n\n private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity\n {\n if (empty($crmData['properties'])) {\n return null;\n }\n\n $crmId = (string) $crmData['id'];\n $properties = $crmData['properties'];\n $associations = $crmData['associations'] ?? [];\n\n $opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(\n $this->config,\n $crmId\n );\n\n if ($opportunityExists) {\n return $this->updateOpportunity($crmId, $properties, $associations);\n }\n\n return $this->createOpportunity($crmId, $properties, $associations);\n }\n\n /**\n * Create new opportunity\n */\n private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n if (! $accountId) {\n return null;\n }\n\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n if (! $businessProcess) {\n return null;\n }\n\n $stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);\n if (! $stage) {\n return null;\n }\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n }\n\n /**\n * Update existing opportunity\n */\n private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n $stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->updateOpportunityAssociations($opportunity, $associations);\n\n return $opportunity;\n }\n\n private function resolveAccountId(array $associations): ?int\n {\n if (! empty($associations['account_id'])) {\n return $associations['account_id'];\n }\n\n if (empty($associations)) {\n return null;\n }\n\n // Fallback: use first company as account (currently SDK returns one company)\n foreach ($associations['companies'] as $accountId) {\n return $accountId;\n }\n\n return null;\n }\n\n private function buildOpportunityData(\n array $properties,\n ?int $accountId,\n ?BusinessProcess $businessProcess,\n ?Stage $stage\n ): array {\n $ownerId = null;\n $profile = null;\n if (! empty($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->getCachedOwnerProfile((string) $ownerId);\n }\n\n $name = 'Unknown';\n if (isset($properties['dealname'])) {\n $name = mb_strimwidth($properties['dealname'], 0, 128);\n }\n\n $amount = $this->resolveAmount($properties);\n $currency = $properties['deal_currency_code'] ?? null;\n\n $closeDate = null;\n if (! empty($properties['closedate'])) {\n $closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');\n }\n\n $remotelyCreatedAt = null;\n if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {\n $date = $this->parseCleanDatetime($properties['createdate']);\n $remotelyCreatedAt = $date?->format('Y-m-d H:i:s');\n }\n\n $closedStages = $this->getClosedDealStages();\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $data = [\n 'team_id' => $this->team->getId(),\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'value' => ! empty($amount) ? $amount : null,\n 'currency_code' => CurrencyFormatter::formatCode($currency),\n 'close_date' => $closeDate,\n 'is_closed' => $isWon || $isLost,\n 'is_won' => $isWon,\n 'remotely_created_at' => $remotelyCreatedAt,\n 'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),\n 'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),\n ];\n\n if ($accountId) {\n $data['account_id'] = $accountId;\n }\n\n if ($stage) {\n $data['stage_id'] = $stage->id;\n }\n\n if ($businessProcess) {\n $recordType = $this->getCachedBusinessProcessRecordType($businessProcess);\n if ($recordType) {\n $data['record_type_id'] = $recordType->id;\n }\n }\n\n return $data;\n }\n\n private function getCachedOwnerProfile(string $ownerId): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $ownerId;\n if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {\n return $this->cachedOwnerProfiles[$cacheKey];\n }\n\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);\n $this->cachedOwnerProfiles[$cacheKey] = $profile;\n\n return $profile;\n }\n\n private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId();\n if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {\n return $this->cachedRecordTypes[$cacheKey];\n }\n\n $recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);\n $this->cachedRecordTypes[$cacheKey] = $recordType;\n\n return $recordType;\n }\n\n private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess\n {\n if ($pipelineId === null) {\n return null;\n }\n\n $cacheKey = $this->getBusinessProcessCacheKey($pipelineId);\n if (isset($this->cachedBusinessProcesses[$cacheKey])) {\n return $this->cachedBusinessProcesses[$cacheKey];\n }\n\n $businessProcess = $this->getBusinessProcess($pipelineId);\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->importStages();\n $businessProcess = $this->getBusinessProcess($pipelineId);\n }\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->logger->info(\n '[HubSpot] Deal is not attached to a pipeline',\n [\n 'pipeline' => $pipelineId]\n );\n }\n\n $this->cachedBusinessProcesses[$cacheKey] = $businessProcess;\n\n return $businessProcess;\n }\n\n private function getBusinessProcess(string $pipelineId): ?BusinessProcess\n {\n return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);\n }\n\n private function getBusinessProcessCacheKey(string $pipelineId): string\n {\n return $this->config->getId() . '_' . $pipelineId;\n }\n\n private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage\n {\n if (empty($stageId)) {\n return null;\n }\n\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;\n if (isset($this->cachedStages[$cacheKey])) {\n return $this->cachedStages[$cacheKey];\n }\n\n $stage = $this->crmEntityRepository->getPipelineStageByConditions(\n $businessProcess,\n [\n 'crm_provider_id' => $stageId,\n 'type' => Stage::TYPE_OPPORTUNITY,\n ]\n );\n\n if ($stage === null) {\n $this->importStages(null, $stageId);\n }\n\n if ($stage === null) {\n $this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);\n }\n\n $this->cachedStages[$cacheKey] = $stage;\n\n return $stage;\n }\n\n private function resolveAmount(array $properties): ?string\n {\n $amount = null;\n if (! empty($properties['amount'])) {\n $amount = str_replace(',', '', $properties['amount']);\n }\n\n if ($this->config->hasDefaultCurrencyFieldSet()) {\n $valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();\n $amount = $properties[$valueFieldName] ?? $amount;\n }\n\n return $amount;\n }\n\n private function parseCleanDatetime(string $datetime): ?Carbon\n {\n // Treat pre-1980 values as invalid\n $minValidDate = Carbon::parse('1980-01-01 00:00:00');\n\n try {\n $date = Carbon::parse($datetime);\n\n if ($minValidDate->gt($date)) {\n return null;\n }\n\n return $date;\n } catch (Exception) {\n return null; // On parse error, treat as null\n }\n }\n\n private function resolveDealProbability(?string $stageProbability): int\n {\n if ($stageProbability === null) {\n return 0;\n }\n\n $probability = (float) $stageProbability;\n\n return $probability > 1 ? 0 : (int) ($probability * 100);\n }\n\n private function resolveForecastCategory(?string $forecastCategory): string\n {\n if (! $forecastCategory) {\n return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;\n }\n\n $forecastCategory = str_replace('_', ' ', $forecastCategory);\n\n return ucwords(strtolower($forecastCategory));\n }\n\n private function importExternalFieldData(array $properties, int $opportunityId): void\n {\n $this->importOpportunityCrmFieldData(\n $properties,\n $this->getCachedOpportunitySyncableFields(),\n $opportunityId\n );\n }\n\n private function getCachedOpportunitySyncableFields(): array\n {\n $cacheKey = (string) $this->config->getId();\n if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {\n $this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();\n }\n\n return $this->cachedOpportunitySyncableFields[$cacheKey];\n }\n\n private function importOpportunityContacts(Opportunity $opportunity, array $associations): void\n {\n // Handle empty or missing contact associations\n if (empty($associations)) {\n // Remove all existing contact associations if none provided\n $this->removeAllOpportunityContacts($opportunity);\n\n return;\n }\n\n // Use differential sync approach for better performance and accuracy\n $this->syncOpportunityContactsDifferential($opportunity, $associations);\n }\n\n /**\n * Sync opportunity contacts using differential approach\n * This compares current vs new associations and only makes necessary changes\n */\n private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void\n {\n $currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);\n $contactAssociationIds = array_keys($contactAssociations);\n\n $contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);\n $contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);\n\n if (empty($contactsToAdd) && empty($contactsToRemove)) {\n return;\n }\n\n $this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);\n\n $this->removeContactAssociations($opportunity, $contactsToRemove);\n $this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);\n }\n\n private function getCurrentContactCrmIds(Opportunity $opportunity): array\n {\n return $opportunity->contacts()\n ->pluck('contacts.crm_provider_id')\n ->toArray();\n }\n\n private function logContactAssociationChanges(\n Opportunity $opportunity,\n array $currentContactCrmIds,\n array $contactAssociations,\n array $contactsToAdd,\n array $contactsToRemove\n ): void {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [\n 'opportunity_id' => $opportunity->getId(),\n 'current_contacts' => $currentContactCrmIds,\n 'new_contacts' => $contactAssociations,\n 'contacts_to_add' => $contactsToAdd,\n 'contacts_to_remove' => $contactsToRemove,\n ]);\n }\n\n private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void\n {\n if (empty($contactsToRemove)) {\n return;\n }\n\n $contactsToDetach = $opportunity->contacts()\n ->whereIn('contacts.crm_provider_id', $contactsToRemove)\n ->pluck('contacts.id')\n ->toArray();\n\n if (! empty($contactsToDetach)) {\n $opportunity->contacts()->detach($contactsToDetach);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_contact_crm_ids' => $contactsToRemove,\n 'removed_contact_count' => count($contactsToDetach),\n ]);\n }\n }\n\n private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void\n {\n if (empty($contactsToAdd)) {\n return;\n }\n\n $contactsAdded = [];\n foreach ($contactsToAdd as $crmId) {\n $id = $contactAssociations[$crmId];\n\n if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {\n $contactsAdded[] = $crmId;\n }\n }\n\n $this->logAddedContacts($opportunity, $contactsAdded);\n }\n\n private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool\n {\n try {\n return $this->performContactAttachment($opportunity, $id, $crmId);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [\n 'opportunity_id' => $opportunity->getId(),\n 'contact_crm_id' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool\n {\n try {\n $opportunity->contacts()->attach($contactId, [\n 'crm_provider_id' => $crmId,\n ]);\n\n return true;\n } catch (\\Illuminate\\Database\\QueryException $e) {\n if (str_contains($e->getMessage(), 'Duplicate entry')) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [\n 'contact_id' => $contactId,\n 'contact_crm_id' => $crmId,\n 'opportunity_id' => $opportunity->getId(),\n ]);\n\n return false;\n }\n\n throw $e;\n }\n }\n\n private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void\n {\n if (! empty($contactsAdded)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'added_contact_crm_ids' => $contactsAdded,\n 'added_contacts_count' => count($contactsAdded),\n ]);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6525595263172666239
|
-8493339382995643936
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
32
6
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use HubSpot\Client\Crm\Deals\Model\CollectionResponseAssociatedId;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Account;
use Exception;
use Jiminny\Component\DealInsights\Forecast\Forecast;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Exceptions\CrmException;
use Jiminny\Models\Opportunity;
use Illuminate\Support\Collection;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Crm\Hubspot\DealFieldsService;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\HubspotSingleSyncStrategy;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Utils\CurrencyFormatter;
/**
* Optimized sync methods for better performance
* These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains
*/
trait OpportunitySyncTrait
{
private const int BATCH_SIZE = 100;
private const int BATCH_PROCESS_SIZE = 800;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected DealFieldsService $dealFieldsService;
private ?array $cachedClosedDealStages = null;
private array $cachedBusinessProcesses = [];
private array $cachedStages = [];
/** @var array<string, array<string>> keyed by config id */
private array $cachedOpportunitySyncableFields = [];
/** @var array<string, mixed> keyed by configId:ownerId */
private array $cachedOwnerProfiles = [];
/** @var array<string, mixed> keyed by configId:businessProcessId */
private array $cachedRecordTypes = [];
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$startTime = microtime(true);
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$parameters['config'] = $this->config;
$syncCount = 0;
$reportedTotal = 0;
$lastSyncedId = [];
$strategyNames = [];
try {
foreach ($strategies as $strategyName => $syncStrategy) {
$strategyNames[] = $strategyName;
$this->logger->info(
'[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,
['team' => $this->team->getId()]
);
$total = 0;
$lastId = null;
$buffer = [];
// HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies
foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {
$buffer[] = $hsOpportunity;
// process every 800 rows (fits < 1 000 association limit)
if (\count($buffer) >= self::BATCH_PROCESS_SIZE) {
$syncCount += $this->processOpportunityBatch($buffer);
$buffer = [];
}
}
// leftovers
if ($buffer) {
$syncCount += $this->processOpportunityBatch($buffer);
}
$reportedTotal += $total;
$lastSyncedId = $lastId;
}
} catch (\HubSpot\Client\Crm\Deals\ApiException | CrmException $e) {
$this->handleSyncException($e, $parameters);
}
$durationMs = round((microtime(true) - $startTime) * 1000, 2);
$this->logger->info(
'[HubSpot] Synced opportunities',
[
'team' => $this->team->getId(),
'strategies' => implode(',', $strategyNames),
'sync_count' => $syncCount,
'total' => $reportedTotal,
'last_synced_id' => $lastSyncedId,
'duration_ms' => $durationMs,
]
);
return $reportedTotal;
}
private function handleSyncException(\Throwable $e, array $parameters): void
{
if (($parameters['since'] ?? null) instanceof Carbon) {
$parameters['since'] = $parameters['since']->toDateTimeString();
}
$parameters['config'] = $this->config->getId();
$this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [
'teamId' => $this->team->getUuid(),
'parameters' => $parameters,
'reason' => $e->getMessage(),
]);
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = [
'config' => $this->config,
'crm_id' => $crmId,
];
try {
if (! $strategy instanceof HubspotSingleSyncStrategy) {
throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');
}
$hsOpportunity = $strategy->fetchOpportunity($parameters);
} catch (\HubSpot\Client\Crm\Deals\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
$hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);
return $this->importOrUpdateOpportunity($hsOpportunity);
}
/**
* Process webhook-collected opportunity batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportOpportunityBatch jobs for batch processing.
*
* @return int Number of opportunity IDs dispatched to jobs
*/
public function batchSyncOpportunities(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
}
/**
* Import a batch of opportunities by their CRM IDs.
* Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().
*
* @param array<string> $crmIds HubSpot deal CRM IDs
*
* @return array{success: array, failed_ids: array, errors?: array<string, string>}
*/
public function importOpportunityBatchByIds(array $crmIds): array
{
$fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);
$allDeals = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
$deals = $this->client->getOpportunitiesByIds($chunk, $fields);
foreach ($deals as $deal) {
$allDeals[] = $deal;
}
}
// IDs not returned by HubSpot are likely deleted or inaccessible deals.
// These are not failures — retrying won't bring them back.
$fetchedIds = array_map('strval', array_column($allDeals, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allDeals),
]);
}
if (empty($allDeals)) {
return ['success' => [], 'failed_ids' => []];
}
return $this->importOpportunityBatch($allDeals);
}
private function getClosedDealStages(): array
{
if ($this->cachedClosedDealStages !== null) {
return $this->cachedClosedDealStages;
}
$stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);
$data = [
'lost' => [],
'won' => [],
];
foreach ($stages as $stage) {
if ($stage->probability == 0.00) {
$data['lost'][] = $stage->crm_provider_id;
}
if ($stage->probability == 100.00) {
$data['won'][] = $stage->crm_provider_id;
}
}
$this->cachedClosedDealStages = $data;
return $data;
}
/**
* Import deals into the database with pre-fetched associations.
*
* API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT
* caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()
* where Laravel retries the whole job with backoff. After all retries exhausted,
* failed() requeues all IDs to Redis.
*
* The per-deal loop catches exceptions individually. A deal can end up in three states:
* - success: imported/updated successfully
* - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)
* These are permanent issues — retrying won't fix them.
* - skipped (null): missing dependencies (no account, unknown pipeline/stage).
* This is acceptable — the deal cannot be imported until those exist.
*/
private function importOpportunityBatch(array $deals): array
{
$syncedOpportunities = [
'success' => [],
'failed_ids' => [],
];
$dealIds = array_column($deals, 'id');
$batchStart = microtime(true);
$slowDeals = [];
// Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the
// queue job retries the whole batch and eventually requeues all deal IDs back to Redis.
try {
$companyAssocStart = microtime(true);
$companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');
$companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);
$contactAssocStart = microtime(true);
$contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');
$contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);
$prepareStart = microtime(true);
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
$prepareTimings = [];
$associationsData = $this->prepareAssociatedEntities(
$companyAssociations,
$contactAssociations,
$prepareTimings
);
$prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);
$missingCompanies = count(array_diff(
$allCompanyIds,
array_keys($associationsData['company_id_mappings'] ?? [])
));
$missingContacts = count(array_diff(
$allContactIds,
array_keys($associationsData['contact_id_mappings'] ?? [])
));
$existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(
$this->config,
array_map('strval', $dealIds)
);
$existingCrmIdSet = array_flip($existingCrmIds);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [
'teamId' => $this->team->getId(),
'dealCount' => count($dealIds),
'error' => $e->getMessage(),
]);
throw $e;
}
$loopStart = microtime(true);
foreach ($deals as $deal) {
$dealStart = microtime(true);
try {
$deal['associations'] = $this->prepareAssociationsForOpportunity(
$deal['id'],
$companyAssociations,
$contactAssociations,
$associationsData
);
$syncedOpportunity = $this->importOrUpdateOpportunity(
$deal,
isset($existingCrmIdSet[(string) $deal['id']])
);
if ($syncedOpportunity) {
$syncedOpportunities['success'][] = $syncedOpportunity;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [
'teamId' => $this->team->getId(),
'crmId' => $deal['id'],
'error' => $e->getMessage(),
]);
$syncedOpportunities['failed_ids'][] = $deal['id'];
$syncedOpportunities['errors'][$deal['id']] = $e->getMessage();
}
$dealMs = (int) round((microtime(true) - $dealStart) * 1000);
if ($dealMs > 1000) {
$slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [
'teamId' => $this->team->getId(),
'deal_count' => count($deals),
'total_ms' => $totalMs,
'company_assoc_api_ms' => $companyAssocMs,
'contact_assoc_api_ms' => $contactAssocMs,
'prepare_entities_ms' => $prepareMs,
'prepare_accounts_ms' => $prepareTimings['accounts_ms'],
'prepare_contacts_ms' => $prepareTimings['contacts_ms'],
'total_companies' => count($allCompanyIds),
'missing_companies' => $missingCompanies,
'total_contacts' => count($allContactIds),
'missing_contacts' => $missingContacts,
'deals_loop_ms' => $loopMs,
'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,
'slow_deals_count' => count($slowDeals),
'slow_deals' => array_slice($slowDeals, 0, 10),
]);
return $syncedOpportunities;
}
/**
* Prepare associated entities for opportunities with optimized batch processing
* Returns structured data with CRM ID to DB ID mappings for each opportunity
*/
private function prepareAssociatedEntities(
array $companyAssociations,
array $contactAssociations,
array &$timings = []
): array {
// Step 1: Collect all unique company and contact IDs from associations
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
// Step 2: Batch sync missing entities and get CRM ID to DB ID mappings
$companyIdMappings = [];
$contactIdMappings = [];
$accountsMs = 0;
$contactsMs = 0;
if (! empty($allCompanyIds)) {
$start = microtime(true);
$companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);
$accountsMs = (int) round((microtime(true) - $start) * 1000);
}
if (! empty($allContactIds)) {
$start = microtime(true);
$contactIdMappings = $this->prepareAssociatedContacts($allContactIds);
$contactsMs = (int) round((microtime(true) - $start) * 1000);
}
$timings = [
'accounts_ms' => $accountsMs,
'contacts_ms' => $contactsMs,
];
return [
'company_id_mappings' => $companyIdMappings,
'contact_id_mappings' => $contactIdMappings,
];
}
/**
* Flatten association data to get unique IDs
*/
private function flattenAssociationIds(array $associations): array
{
$ids = [];
foreach ($associations as $dealAssociations) {
if (is_array($dealAssociations)) {
foreach ($dealAssociations as $id) {
$ids[$id] = true;
}
}
}
return array_keys($ids);
}
/**
* Batch sync missing accounts
*/
private function prepareAssociatedAccounts(array $companyIds): array
{
// Find which accounts already exist (lean covering-index lookup)
$existingAccountsData = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));
if (empty($missingCompanyIds)) {
return $existingAccountsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [
'teamId' => $this->team->getUuid(),
'total_companies' => count($companyIds),
'existing_companies' => count($existingAccountsData),
'missing_companies' => count($missingCompanyIds),
]);
// we already have limit on opportunity ids count
// Initialize variable before try block
$syncedAccountsData = [];
try {
$syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [
'size' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
$syncedAccountsData = [];
}
return $existingAccountsData + $syncedAccountsData;
}
/**
* Prepare associated contacts - find existing and sync missing ones
* Returns mapping of CRM ID to DB ID
*/
private function prepareAssociatedContacts(array $contactIds): array
{
// Find which contacts already exist (lean covering-index lookup)
$existingContactsData = $this->crmEntityRepository
->getExistingContactIdsMap($this->config, $contactIds);
$missingContactIds = array_diff($contactIds, array_keys($existingContactsData));
if (empty($missingContactIds)) {
return $existingContactsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [
'teamId' => $this->team->getUuid(),
'total_contacts' => count($contactIds),
'existing_contacts' => count($existingContactsData),
'missing_contacts' => count($missingContactIds),
]);
// Sync missing contacts using batch API
try {
$syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [
'size' => count($missingContactIds),
'error' => $e->getMessage(),
]);
$syncedContactsData = [];
}
return $existingContactsData + $syncedContactsData;
}
private function batchSyncCrmObjects(string $objectType, array $crmIds): array
{
$syncObjects = [];
$crmObjectIds = array_values($crmIds);
foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {
try {
$objects = $objectType === 'companies' ?
$this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :
$this->client->getContactsByIds($chunk, $this->getContactFields());
foreach ($objects as $objectId => $objectData) {
$this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [
'requested_count' => count($chunk),
'synced_count' => count($objects),
]);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [
'ids' => $chunk,
'error' => $e->getMessage(),
]);
}
}
return $syncObjects;
}
private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void
{
try {
$object = $objectType === 'companies' ?
$this->importAccount($objectData) :
$this->importContact($objectData);
if ($object) {
$syncObjects[$object->getCrmProviderId()] = $object->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [
'id' => $objectId,
'error' => $e->getMessage(),
]);
}
}
/**
* Prepare associations for a single opportunity
*
* The return value is an array with the following structure:
* [
* 'companies' => [
* $companyCrmId => $companyId,
* ...
* ],
* 'contacts' => [
* $contactCrmId => $contactId,
* ...
* ],
* 'account_id' => $accountId,
* ]
*/
private function prepareAssociationsForOpportunity(
string $oppCrmId,
array $companyAssociations,
array $contactAssociations,
array $associationsData
): array {
$associations = [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
$oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];
foreach ($oppCompanyIds as $companyCrmId) {
if (isset($associationsData['company_id_mappings'][$companyCrmId])) {
$associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];
// Set primary account (first company becomes primary account)
if ($associations['account_id'] === null) {
$associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];
}
}
}
$oppContactIds = $contactAssociations[$oppCrmId] ?? [];
foreach ($oppContactIds as $contactCrmId) {
if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {
$associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];
}
}
return $associations;
}
/**
* Update only associations for an opportunity
*/
private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void
{
// Update contact associations
$this->importOpportunityContacts($opportunity, $associations['contacts']);
// Update company (account) associations
$this->updateOpportunityAccount($opportunity, $associations['account_id']);
}
/**
* Remove all contact associations from an opportunity
*/
private function removeAllOpportunityContacts(Opportunity $opportunity): void
{
$currentCount = (int) $opportunity->contacts()->count();
if ($currentCount > 0) {
$opportunity->contacts()->detach();
$this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_count' => $currentCount,
]);
}
}
private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void
{
if ($accountId === null) {
// No account ID provided - keep current account
return;
}
$currentAccountId = $opportunity->getAccountId();
// Only update if account has changed
if ($currentAccountId !== $accountId) {
$opportunity->account_id = $accountId;
$opportunity->save();
$this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [
'opportunity_id' => $opportunity->getId(),
'old_account_id' => $currentAccountId,
'new_account_id' => $accountId,
]);
}
}
/**
* Find existing opportunities by external IDs (OPTIMIZED VERSION)
* Uses batch query for better performance
*/
private function findExistingOpportunities(array $crmIds): Collection
{
return $this->crmEntityRepository
->findOpportunitiesByExternalIds($this->config, $crmIds);
}
private function processOpportunityBatch(array $opportunities): int
{
$syncedOpportunities = $this->importOpportunityBatch($opportunities);
return count($syncedOpportunities['success'] ?? []);
}
/**
* Convert single deal associations from HubSpot format to internal format
* Handles both HubSpot SDK objects and array formats
*
* @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed
*
* @return array Processed associations with DB IDs
*/
private function convertDealAssociations(array $opportunityAssociations): array
{
$associations = $this->initializeAssociationsStructure();
if (empty($opportunityAssociations)) {
return $associations;
}
$associationIds = $this->extractAssociationIds($opportunityAssociations);
$this->processCompanyAssociations($associationIds, $associations);
$this->processContactAssociations($associationIds, $associations);
return $associations;
}
private function initializeAssociationsStructure(): array
{
return [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
}
private function extractAssociationIds(array $opportunityAssociations): array
{
$associationIds = [];
foreach ($opportunityAssociations as $type => $associationData) {
if (! empty($associationData)) {
$associationIds[$type] = $this->convertSingleDealAssociations($associationData);
}
}
return $associationIds;
}
private function processCompanyAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['companies'])) {
return;
}
$companyId = $associationIds['companies'][0];
$account = $this->findOrSyncAccount($companyId);
if ($account instanceof Account) {
$associations['companies'][$companyId] = $account->getId();
$associations['account_id'] = $account->getId();
}
}
private function processContactAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['contacts'])) {
return;
}
foreach ($associationIds['contacts'] as $contactId) {
$contact = $this->findOrSyncContact($contactId);
if ($contact instanceof Contact) {
$associations['contacts'][$contactId] = $contact->getId();
}
}
}
private function findOrSyncAccount(string $companyId): ?Account
{
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);
if (! $account instanceof Account) {
$account = $this->syncAccount($companyId);
}
return $account;
}
private function findOrSyncContact(string $contactId): ?Contact
{
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);
if (! $contact instanceof Contact) {
$contact = $this->syncContact($contactId);
}
return $contact;
}
private function convertSingleDealAssociations($opportunityAssociations = null): array
{
$associationData = [];
if ($opportunityAssociations === null) {
return $associationData;
}
// Handle array input (from extractAssociationIds)
if (is_array($opportunityAssociations)) {
return $opportunityAssociations;
}
// Handle CollectionResponseAssociatedId object
if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {
foreach ($opportunityAssociations->getResults() as $association) {
$associationData[] = $association->getId();
}
}
return $associationData;
}
private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity
{
if (empty($crmData['properties'])) {
return null;
}
$crmId = (string) $crmData['id'];
$properties = $crmData['properties'];
$associations = $crmData['associations'] ?? [];
$opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(
$this->config,
$crmId
);
if ($opportunityExists) {
return $this->updateOpportunity($crmId, $properties, $associations);
}
return $this->createOpportunity($crmId, $properties, $associations);
}
/**
* Create new opportunity
*/
private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity
{
$accountId = $this->resolveAccountId($associations);
if (! $accountId) {
return null;
}
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
if (! $businessProcess) {
return null;
}
$stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);
if (! $stage) {
return null;
}
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->importOpportunityContacts($opportunity, $associations['contacts']);
if ($opportunity->wasRecentlyCreated) {
MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());
}
return $opportunity;
}
/**
* Update existing opportunity
*/
private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity
{
$accountId = $this->resolveAccountId($associations);
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
$stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->updateOpportunityAssociations($opportunity, $associations);
return $opportunity;
}
private function resolveAccountId(array $associations): ?int
{
if (! empty($associations['account_id'])) {
return $associations['account_id'];
}
if (empty($associations)) {
return null;
}
// Fallback: use first company as account (currently SDK returns one company)
foreach ($associations['companies'] as $accountId) {
return $accountId;
}
return null;
}
private function buildOpportunityData(
array $properties,
?int $accountId,
?BusinessProcess $businessProcess,
?Stage $stage
): array {
$ownerId = null;
$profile = null;
if (! empty($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->getCachedOwnerProfile((string) $ownerId);
}
$name = 'Unknown';
if (isset($properties['dealname'])) {
$name = mb_strimwidth($properties['dealname'], 0, 128);
}
$amount = $this->resolveAmount($properties);
$currency = $properties['deal_currency_code'] ?? null;
$closeDate = null;
if (! empty($properties['closedate'])) {
$closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');
}
$remotelyCreatedAt = null;
if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {
$date = $this->parseCleanDatetime($properties['createdate']);
$remotelyCreatedAt = $date?->format('Y-m-d H:i:s');
}
$closedStages = $this->getClosedDealStages();
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$data = [
'team_id' => $this->team->getId(),
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => $name,
'value' => ! empty($amount) ? $amount : null,
'currency_code' => CurrencyFormatter::formatCode($currency),
'close_date' => $closeDate,
'is_closed' => $isWon || $isLost,
'is_won' => $isWon,
'remotely_created_at' => $remotelyCreatedAt,
'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),
'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),
];
if ($accountId) {
$data['account_id'] = $accountId;
}
if ($stage) {
$data['stage_id'] = $stage->id;
}
if ($businessProcess) {
$recordType = $this->getCachedBusinessProcessRecordType($businessProcess);
if ($recordType) {
$data['record_type_id'] = $recordType->id;
}
}
return $data;
}
private function getCachedOwnerProfile(string $ownerId): mixed
{
$cacheKey = $this->config->getId() . ':' . $ownerId;
if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {
return $this->cachedOwnerProfiles[$cacheKey];
}
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);
$this->cachedOwnerProfiles[$cacheKey] = $profile;
return $profile;
}
private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed
{
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId();
if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {
return $this->cachedRecordTypes[$cacheKey];
}
$recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);
$this->cachedRecordTypes[$cacheKey] = $recordType;
return $recordType;
}
private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess
{
if ($pipelineId === null) {
return null;
}
$cacheKey = $this->getBusinessProcessCacheKey($pipelineId);
if (isset($this->cachedBusinessProcesses[$cacheKey])) {
return $this->cachedBusinessProcesses[$cacheKey];
}
$businessProcess = $this->getBusinessProcess($pipelineId);
if (! $businessProcess instanceof BusinessProcess) {
$this->importStages();
$businessProcess = $this->getBusinessProcess($pipelineId);
}
if (! $businessProcess instanceof BusinessProcess) {
$this->logger->info(
'[HubSpot] Deal is not attached to a pipeline',
[
'pipeline' => $pipelineId]
);
}
$this->cachedBusinessProcesses[$cacheKey] = $businessProcess;
return $businessProcess;
}
private function getBusinessProcess(string $pipelineId): ?BusinessProcess
{
return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);
}
private function getBusinessProcessCacheKey(string $pipelineId): string
{
return $this->config->getId() . '_' . $pipelineId;
}
private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage
{
if (empty($stageId)) {
return null;
}
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;
if (isset($this->cachedStages[$cacheKey])) {
return $this->cachedStages[$cacheKey];
}
$stage = $this->crmEntityRepository->getPipelineStageByConditions(
$businessProcess,
[
'crm_provider_id' => $stageId,
'type' => Stage::TYPE_OPPORTUNITY,
]
);
if ($stage === null) {
$this->importStages(null, $stageId);
}
if ($stage === null) {
$this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);
}
$this->cachedStages[$cacheKey] = $stage;
return $stage;
}
private function resolveAmount(array $properties): ?string
{
$amount = null;
if (! empty($properties['amount'])) {
$amount = str_replace(',', '', $properties['amount']);
}
if ($this->config->hasDefaultCurrencyFieldSet()) {
$valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();
$amount = $properties[$valueFieldName] ?? $amount;
}
return $amount;
}
private function parseCleanDatetime(string $datetime): ?Carbon
{
// Treat pre-1980 values as invalid
$minValidDate = Carbon::parse('1980-01-01 00:00:00');
try {
$date = Carbon::parse($datetime);
if ($minValidDate->gt($date)) {
return null;
}
return $date;
} catch (Exception) {
return null; // On parse error, treat as null
}
}
private function resolveDealProbability(?string $stageProbability): int
{
if ($stageProbability === null) {
return 0;
}
$probability = (float) $stageProbability;
return $probability > 1 ? 0 : (int) ($probability * 100);
}
private function resolveForecastCategory(?string $forecastCategory): string
{
if (! $forecastCategory) {
return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;
}
$forecastCategory = str_replace('_', ' ', $forecastCategory);
return ucwords(strtolower($forecastCategory));
}
private function importExternalFieldData(array $properties, int $opportunityId): void
{
$this->importOpportunityCrmFieldData(
$properties,
$this->getCachedOpportunitySyncableFields(),
$opportunityId
);
}
private function getCachedOpportunitySyncableFields(): array
{
$cacheKey = (string) $this->config->getId();
if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {
$this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();
}
return $this->cachedOpportunitySyncableFields[$cacheKey];
}
private function importOpportunityContacts(Opportunity $opportunity, array $associations): void
{
// Handle empty or missing contact associations
if (empty($associations)) {
// Remove all existing contact associations if none provided
$this->removeAllOpportunityContacts($opportunity);
return;
}
// Use differential sync approach for better performance and accuracy
$this->syncOpportunityContactsDifferential($opportunity, $associations);
}
/**
* Sync opportunity contacts using differential approach
* This compares current vs new associations and only makes necessary changes
*/
private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void
{
$currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);
$contactAssociationIds = array_keys($contactAssociations);
$contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);
$contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);
if (empty($contactsToAdd) && empty($contactsToRemove)) {
return;
}
$this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);
$this->removeContactAssociations($opportunity, $contactsToRemove);
$this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);
}
private function getCurrentContactCrmIds(Opportunity $opportunity): array
{
return $opportunity->contacts()
->pluck('contacts.crm_provider_id')
->toArray();
}
private function logContactAssociationChanges(
Opportunity $opportunity,
array $currentContactCrmIds,
array $contactAssociations,
array $contactsToAdd,
array $contactsToRemove
): void {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [
'opportunity_id' => $opportunity->getId(),
'current_contacts' => $currentContactCrmIds,
'new_contacts' => $contactAssociations,
'contacts_to_add' => $contactsToAdd,
'contacts_to_remove' => $contactsToRemove,
]);
}
private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void
{
if (empty($contactsToRemove)) {
return;
}
$contactsToDetach = $opportunity->contacts()
->whereIn('contacts.crm_provider_id', $contactsToRemove)
->pluck('contacts.id')
->toArray();
if (! empty($contactsToDetach)) {
$opportunity->contacts()->detach($contactsToDetach);
$this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_contact_crm_ids' => $contactsToRemove,
'removed_contact_count' => count($contactsToDetach),
]);
}
}
private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void
{
if (empty($contactsToAdd)) {
return;
}
$contactsAdded = [];
foreach ($contactsToAdd as $crmId) {
$id = $contactAssociations[$crmId];
if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {
$contactsAdded[] = $crmId;
}
}
$this->logAddedContacts($opportunity, $contactsAdded);
}
private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool
{
try {
return $this->performContactAttachment($opportunity, $id, $crmId);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [
'opportunity_id' => $opportunity->getId(),
'contact_crm_id' => $crmId,
'error' => $e->getMessage(),
]);
return false;
}
}
private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool
{
try {
$opportunity->contacts()->attach($contactId, [
'crm_provider_id' => $crmId,
]);
return true;
} catch (\Illuminate\Database\QueryException $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [
'contact_id' => $contactId,
'contact_crm_id' => $crmId,
'opportunity_id' => $opportunity->getId(),
]);
return false;
}
throw $e;
}
}
private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void
{
if (! empty($contactsAdded)) {
$this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [
'opportunity_id' => $opportunity->getId(),
'added_contact_crm_ids' => $contactsAdded,
'added_contacts_count' => count($contactsAdded),
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2758
|
111
|
48
|
2026-05-07T11:39:08.513723+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153948513_m1.jpg...
|
PhpStorm
|
faVsco.js – OpportunitySyncTrait.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
32
2
22
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use HubSpot\Client\Crm\Deals\Model\CollectionResponseAssociatedId;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Account;
use Exception;
use Jiminny\Component\DealInsights\Forecast\Forecast;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Exceptions\CrmException;
use Jiminny\Models\Opportunity;
use Illuminate\Support\Collection;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Crm\Hubspot\DealFieldsService;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\HubspotSingleSyncStrategy;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Utils\CurrencyFormatter;
/**
* Optimized sync methods for better performance
* These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains
*/
trait OpportunitySyncTrait
{
private const int BATCH_SIZE = 100;
private const int BATCH_PROCESS_SIZE = 800;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected DealFieldsService $dealFieldsService;
private ?array $cachedClosedDealStages = null;
private array $cachedBusinessProcesses = [];
private array $cachedStages = [];
/** @var array<string, array<string>> keyed by config id */
private array $cachedOpportunitySyncableFields = [];
/** @var array<string, mixed> keyed by configId:ownerId */
private array $cachedOwnerProfiles = [];
/** @var array<string, mixed> keyed by configId:businessProcessId */
private array $cachedRecordTypes = [];
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$startTime = microtime(true);
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$parameters['config'] = $this->config;
$syncCount = 0;
$reportedTotal = 0;
$lastSyncedId = [];
$strategyNames = [];
try {
foreach ($strategies as $strategyName => $syncStrategy) {
$strategyNames[] = $strategyName;
$this->logger->info(
'[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,
['team' => $this->team->getId()]
);
$total = 0;
$lastId = null;
$buffer = [];
// HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies
foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {
$buffer[] = $hsOpportunity;
// process every 800 rows (fits < 1 000 association limit)
if (\count($buffer) >= self::BATCH_PROCESS_SIZE) {
$syncCount += $this->processOpportunityBatch($buffer);
$buffer = [];
}
}
// leftovers
if ($buffer) {
$syncCount += $this->processOpportunityBatch($buffer);
}
$reportedTotal += $total;
$lastSyncedId = $lastId;
}
} catch (\HubSpot\Client\Crm\Deals\ApiException | CrmException $e) {
$this->handleSyncException($e, $parameters);
}
$durationMs = round((microtime(true) - $startTime) * 1000, 2);
$this->logger->info(
'[HubSpot] Synced opportunities',
[
'team' => $this->team->getId(),
'strategies' => implode(',', $strategyNames),
'sync_count' => $syncCount,
'total' => $reportedTotal,
'last_synced_id' => $lastSyncedId,
'duration_ms' => $durationMs,
]
);
return $reportedTotal;
}
private function handleSyncException(\Throwable $e, array $parameters): void
{
if (($parameters['since'] ?? null) instanceof Carbon) {
$parameters['since'] = $parameters['since']->toDateTimeString();
}
$parameters['config'] = $this->config->getId();
$this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [
'teamId' => $this->team->getUuid(),
'parameters' => $parameters,
'reason' => $e->getMessage(),
]);
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = [
'config' => $this->config,
'crm_id' => $crmId,
];
try {
if (! $strategy instanceof HubspotSingleSyncStrategy) {
throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');
}
$hsOpportunity = $strategy->fetchOpportunity($parameters);
} catch (\HubSpot\Client\Crm\Deals\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
$hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);
return $this->importOrUpdateOpportunity($hsOpportunity);
}
/**
* Process webhook-collected opportunity batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportOpportunityBatch jobs for batch processing.
*
* @return int Number of opportunity IDs dispatched to jobs
*/
public function batchSyncOpportunities(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
}
/**
* Import a batch of opportunities by their CRM IDs.
* Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().
*
* @param array<string> $crmIds HubSpot deal CRM IDs
*
* @return array{success: array, failed_ids: array, errors?: array<string, string>}
*/
public function importOpportunityBatchByIds(array $crmIds): array
{
$fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);
$allDeals = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
$deals = $this->client->getOpportunitiesByIds($chunk, $fields);
foreach ($deals as $deal) {
$allDeals[] = $deal;
}
}
// IDs not returned by HubSpot are likely deleted or inaccessible deals.
// These are not failures — retrying won't bring them back.
$fetchedIds = array_map('strval', array_column($allDeals, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allDeals),
]);
}
if (empty($allDeals)) {
return ['success' => [], 'failed_ids' => []];
}
return $this->importOpportunityBatch($allDeals);
}
private function getClosedDealStages(): array
{
if ($this->cachedClosedDealStages !== null) {
return $this->cachedClosedDealStages;
}
$stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);
$data = [
'lost' => [],
'won' => [],
];
foreach ($stages as $stage) {
if ($stage->probability == 0.00) {
$data['lost'][] = $stage->crm_provider_id;
}
if ($stage->probability == 100.00) {
$data['won'][] = $stage->crm_provider_id;
}
}
$this->cachedClosedDealStages = $data;
return $data;
}
/**
* Import deals into the database with pre-fetched associations.
*
* API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT
* caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()
* where Laravel retries the whole job with backoff. After all retries exhausted,
* failed() requeues all IDs to Redis.
*
* The per-deal loop catches exceptions individually. A deal can end up in three states:
* - success: imported/updated successfully
* - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)
* These are permanent issues — retrying won't fix them.
* - skipped (null): missing dependencies (no account, unknown pipeline/stage).
* This is acceptable — the deal cannot be imported until those exist.
*/
private function importOpportunityBatch(array $deals): array
{
$syncedOpportunities = [
'success' => [],
'failed_ids' => [],
];
$dealIds = array_column($deals, 'id');
$batchStart = microtime(true);
$slowDeals = [];
// Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the
// queue job retries the whole batch and eventually requeues all deal IDs back to Redis.
try {
$companyAssocStart = microtime(true);
$companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');
$companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);
$contactAssocStart = microtime(true);
$contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');
$contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);
$prepareStart = microtime(true);
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
$prepareTimings = [];
$associationsData = $this->prepareAssociatedEntities(
$companyAssociations,
$contactAssociations,
$prepareTimings
);
$prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);
$missingCompanies = count(array_diff(
$allCompanyIds,
array_keys($associationsData['company_id_mappings'] ?? [])
));
$missingContacts = count(array_diff(
$allContactIds,
array_keys($associationsData['contact_id_mappings'] ?? [])
));
$existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(
$this->config,
array_map('strval', $dealIds)
);
$existingCrmIdSet = array_flip($existingCrmIds);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [
'teamId' => $this->team->getId(),
'dealCount' => count($dealIds),
'error' => $e->getMessage(),
]);
throw $e;
}
$loopStart = microtime(true);
foreach ($deals as $deal) {
$dealStart = microtime(true);
try {
$deal['associations'] = $this->prepareAssociationsForOpportunity(
$deal['id'],
$companyAssociations,
$contactAssociations,
$associationsData
);
$syncedOpportunity = $this->importOrUpdateOpportunity(
$deal,
isset($existingCrmIdSet[(string) $deal['id']])
);
if ($syncedOpportunity) {
$syncedOpportunities['success'][] = $syncedOpportunity;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [
'teamId' => $this->team->getId(),
'crmId' => $deal['id'],
'error' => $e->getMessage(),
]);
$syncedOpportunities['failed_ids'][] = $deal['id'];
$syncedOpportunities['errors'][$deal['id']] = $e->getMessage();
}
$dealMs = (int) round((microtime(true) - $dealStart) * 1000);
if ($dealMs > 1000) {
$slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [
'teamId' => $this->team->getId(),
'deal_count' => count($deals),
'total_ms' => $totalMs,
'company_assoc_api_ms' => $companyAssocMs,
'contact_assoc_api_ms' => $contactAssocMs,
'prepare_entities_ms' => $prepareMs,
'prepare_accounts_ms' => $prepareTimings['accounts_ms'],
'prepare_contacts_ms' => $prepareTimings['contacts_ms'],
'total_companies' => count($allCompanyIds),
'missing_companies' => $missingCompanies,
'total_contacts' => count($allContactIds),
'missing_contacts' => $missingContacts,
'deals_loop_ms' => $loopMs,
'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,
'slow_deals_count' => count($slowDeals),
'slow_deals' => array_slice($slowDeals, 0, 10),
]);
return $syncedOpportunities;
}
/**
* Prepare associated entities for opportunities with optimized batch processing
* Returns structured data with CRM ID to DB ID mappings for each opportunity
*/
private function prepareAssociatedEntities(
array $companyAssociations,
array $contactAssociations,
array &$timings = []
): array {
// Step 1: Collect all unique company and contact IDs from associations
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
// Step 2: Batch sync missing entities and get CRM ID to DB ID mappings
$companyIdMappings = [];
$contactIdMappings = [];
$accountsMs = 0;
$contactsMs = 0;
if (! empty($allCompanyIds)) {
$start = microtime(true);
$companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);
$accountsMs = (int) round((microtime(true) - $start) * 1000);
}
if (! empty($allContactIds)) {
$start = microtime(true);
$contactIdMappings = $this->prepareAssociatedContacts($allContactIds);
$contactsMs = (int) round((microtime(true) - $start) * 1000);
}
$timings = [
'accounts_ms' => $accountsMs,
'contacts_ms' => $contactsMs,
];
return [
'company_id_mappings' => $companyIdMappings,
'contact_id_mappings' => $contactIdMappings,
];
}
/**
* Flatten association data to get unique IDs
*/
private function flattenAssociationIds(array $associations): array
{
$ids = [];
foreach ($associations as $dealAssociations) {
if (is_array($dealAssociations)) {
foreach ($dealAssociations as $id) {
$ids[$id] = true;
}
}
}
return array_keys($ids);
}
/**
* Batch sync missing accounts
*/
private function prepareAssociatedAccounts(array $companyIds): array
{
// Find which accounts already exist (lean covering-index lookup)
$existingAccountsData = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));
if (empty($missingCompanyIds)) {
return $existingAccountsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [
'teamId' => $this->team->getUuid(),
'total_companies' => count($companyIds),
'existing_companies' => count($existingAccountsData),
'missing_companies' => count($missingCompanyIds),
]);
// we already have limit on opportunity ids count
// Initialize variable before try block
$syncedAccountsData = [];
try {
$syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [
'size' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
$syncedAccountsData = [];
}
return $existingAccountsData + $syncedAccountsData;
}
/**
* Prepare associated contacts - find existing and sync missing ones
* Returns mapping of CRM ID to DB ID
*/
private function prepareAssociatedContacts(array $contactIds): array
{
// Find which contacts already exist (lean covering-index lookup)
$existingContactsData = $this->crmEntityRepository
->getExistingContactIdsMap($this->config, $contactIds);
$missingContactIds = array_diff($contactIds, array_keys($existingContactsData));
if (empty($missingContactIds)) {
return $existingContactsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [
'teamId' => $this->team->getUuid(),
'total_contacts' => count($contactIds),
'existing_contacts' => count($existingContactsData),
'missing_contacts' => count($missingContactIds),
]);
// Sync missing contacts using batch API
try {
$syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [
'size' => count($missingContactIds),
'error' => $e->getMessage(),
]);
$syncedContactsData = [];
}
return $existingContactsData + $syncedContactsData;
}
private function batchSyncCrmObjects(string $objectType, array $crmIds): array
{
$syncObjects = [];
$crmObjectIds = array_values($crmIds);
foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {
try {
$objects = $objectType === 'companies' ?
$this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :
$this->client->getContactsByIds($chunk, $this->getContactFields());
foreach ($objects as $objectId => $objectData) {
$this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [
'requested_count' => count($chunk),
'synced_count' => count($objects),
]);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [
'ids' => $chunk,
'error' => $e->getMessage(),
]);
}
}
return $syncObjects;
}
private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void
{
try {
$object = $objectType === 'companies' ?
$this->importAccount($objectData) :
$this->importContact($objectData);
if ($object) {
$syncObjects[$object->getCrmProviderId()] = $object->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [
'id' => $objectId,
'error' => $e->getMessage(),
]);
}
}
/**
* Prepare associations for a single opportunity
*
* The return value is an array with the following structure:
* [
* 'companies' => [
* $companyCrmId => $companyId,
* ...
* ],
* 'contacts' => [
* $contactCrmId => $contactId,
* ...
* ],
* 'account_id' => $accountId,
* ]
*/
private function prepareAssociationsForOpportunity(
string $oppCrmId,
array $companyAssociations,
array $contactAssociations,
array $associationsData
): array {
$associations = [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
$oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];
foreach ($oppCompanyIds as $companyCrmId) {
if (isset($associationsData['company_id_mappings'][$companyCrmId])) {
$associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];
// Set primary account (first company becomes primary account)
if ($associations['account_id'] === null) {
$associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];
}
}
}
$oppContactIds = $contactAssociations[$oppCrmId] ?? [];
foreach ($oppContactIds as $contactCrmId) {
if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {
$associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];
}
}
return $associations;
}
/**
* Update only associations for an opportunity
*/
private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void
{
// Update contact associations
$this->importOpportunityContacts($opportunity, $associations['contacts']);
// Update company (account) associations
$this->updateOpportunityAccount($opportunity, $associations['account_id']);
}
/**
* Remove all contact associations from an opportunity
*/
private function removeAllOpportunityContacts(Opportunity $opportunity): void
{
$currentCount = (int) $opportunity->contacts()->count();
if ($currentCount > 0) {
$opportunity->contacts()->detach();
$this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_count' => $currentCount,
]);
}
}
private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void
{
if ($accountId === null) {
// No account ID provided - keep current account
return;
}
$currentAccountId = $opportunity->getAccountId();
// Only update if account has changed
if ($currentAccountId !== $accountId) {
$opportunity->account_id = $accountId;
$opportunity->save();
$this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [
'opportunity_id' => $opportunity->getId(),
'old_account_id' => $currentAccountId,
'new_account_id' => $accountId,
]);
}
}
/**
* Find existing opportunities by external IDs (OPTIMIZED VERSION)
* Uses batch query for better performance
*/
private function findExistingOpportunities(array $crmIds): Collection
{
return $this->crmEntityRepository
->findOpportunitiesByExternalIds($this->config, $crmIds);
}
private function processOpportunityBatch(array $opportunities): int
{
$syncedOpportunities = $this->importOpportunityBatch($opportunities);
return count($syncedOpportunities['success'] ?? []);
}
/**
* Convert single deal associations from HubSpot format to internal format
* Handles both HubSpot SDK objects and array formats
*
* @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed
*
* @return array Processed associations with DB IDs
*/
private function convertDealAssociations(array $opportunityAssociations): array
{
$associations = $this->initializeAssociationsStructure();
if (empty($opportunityAssociations)) {
return $associations;
}
$associationIds = $this->extractAssociationIds($opportunityAssociations);
$this->processCompanyAssociations($associationIds, $associations);
$this->processContactAssociations($associationIds, $associations);
return $associations;
}
private function initializeAssociationsStructure(): array
{
return [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
}
private function extractAssociationIds(array $opportunityAssociations): array
{
$associationIds = [];
foreach ($opportunityAssociations as $type => $associationData) {
if (! empty($associationData)) {
$associationIds[$type] = $this->convertSingleDealAssociations($associationData);
}
}
return $associationIds;
}
private function processCompanyAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['companies'])) {
return;
}
$companyId = $associationIds['companies'][0];
$account = $this->findOrSyncAccount($companyId);
if ($account instanceof Account) {
$associations['companies'][$companyId] = $account->getId();
$associations['account_id'] = $account->getId();
}
}
private function processContactAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['contacts'])) {
return;
}
foreach ($associationIds['contacts'] as $contactId) {
$contact = $this->findOrSyncContact($contactId);
if ($contact instanceof Contact) {
$associations['contacts'][$contactId] = $contact->getId();
}
}
}
private function findOrSyncAccount(string $companyId): ?Account
{
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);
if (! $account instanceof Account) {
$account = $this->syncAccount($companyId);
}
return $account;
}
private function findOrSyncContact(string $contactId): ?Contact
{
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);
if (! $contact instanceof Contact) {
$contact = $this->syncContact($contactId);
}
return $contact;
}
private function convertSingleDealAssociations($opportunityAssociations = null): array
{
$associationData = [];
if ($opportunityAssociations === null) {
return $associationData;
}
// Handle array input (from extractAssociationIds)
if (is_array($opportunityAssociations)) {
return $opportunityAssociations;
}
// Handle CollectionResponseAssociatedId object
if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {
foreach ($opportunityAssociations->getResults() as $association) {
$associationData[] = $association->getId();
}
}
return $associationData;
}
private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity
{
if (empty($crmData['properties'])) {
return null;
}
$crmId = (string) $crmData['id'];
$properties = $crmData['properties'];
$associations = $crmData['associations'] ?? [];
$opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(
$this->config,
$crmId
);
if ($opportunityExists) {
return $this->updateOpportunity($crmId, $properties, $associations);
}
return $this->createOpportunity($crmId, $properties, $associations);
}
/**
* Create new opportunity
*/
private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity
{
$accountId = $this->resolveAccountId($associations);
if (! $accountId) {
return null;
}
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
if (! $businessProcess) {
return null;
}
$stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);
if (! $stage) {
return null;
}
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->importOpportunityContacts($opportunity, $associations['contacts']);
if ($opportunity->wasRecentlyCreated) {
MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());
}
return $opportunity;
}
/**
* Update existing opportunity
*/
private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity
{
$accountId = $this->resolveAccountId($associations);
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
$stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->updateOpportunityAssociations($opportunity, $associations);
return $opportunity;
}
private function resolveAccountId(array $associations): ?int
{
if (! empty($associations['account_id'])) {
return $associations['account_id'];
}
if (empty($associations)) {
return null;
}
// Fallback: use first company as account (currently SDK returns one company)
foreach ($associations['companies'] as $accountId) {
return $accountId;
}
return null;
}
private function buildOpportunityData(
array $properties,
?int $accountId,
?BusinessProcess $businessProcess,
?Stage $stage
): array {
$ownerId = null;
$profile = null;
if (! empty($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->getCachedOwnerProfile((string) $ownerId);
}
$name = 'Unknown';
if (isset($properties['dealname'])) {
$name = mb_strimwidth($properties['dealname'], 0, 128);
}
$amount = $this->resolveAmount($properties);
$currency = $properties['deal_currency_code'] ?? null;
$closeDate = null;
if (! empty($properties['closedate'])) {
$closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');
}
$remotelyCreatedAt = null;
if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {
$date = $this->parseCleanDatetime($properties['createdate']);
$remotelyCreatedAt = $date?->format('Y-m-d H:i:s');
}
$closedStages = $this->getClosedDealStages();
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$data = [
'team_id' => $this->team->getId(),
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => $name,
'value' => ! empty($amount) ? $amount : null,
'currency_code' => CurrencyFormatter::formatCode($currency),
'close_date' => $closeDate,
'is_closed' => $isWon || $isLost,
'is_won' => $isWon,
'remotely_created_at' => $remotelyCreatedAt,
'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),
'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),
];
if ($accountId) {
$data['account_id'] = $accountId;
}
if ($stage) {
$data['stage_id'] = $stage->id;
}
if ($businessProcess) {
$recordType = $this->getCachedBusinessProcessRecordType($businessProcess);
if ($recordType) {
$data['record_type_id'] = $recordType->id;
}
}
return $data;
}
private function getCachedOwnerProfile(string $ownerId): mixed
{
$cacheKey = $this->config->getId() . ':' . $ownerId;
if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {
return $this->cachedOwnerProfiles[$cacheKey];
}
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);
$this->cachedOwnerProfiles[$cacheKey] = $profile;
return $profile;
}
private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed
{
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId();
if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {
return $this->cachedRecordTypes[$cacheKey];
}
$recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);
$this->cachedRecordTypes[$cacheKey] = $recordType;
return $recordType;
}
private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess
{
if ($pipelineId === null) {
return null;
}
$cacheKey = $this->getBusinessProcessCacheKey($pipelineId);
if (isset($this->cachedBusinessProcesses[$cacheKey])) {
return $this->cachedBusinessProcesses[$cacheKey];
}
$businessProcess = $this->getBusinessProcess($pipelineId);
if (! $businessProcess instanceof BusinessProcess) {
$this->importStages();
$businessProcess = $this->getBusinessProcess($pipelineId);
}
if (! $businessProcess instanceof BusinessProcess) {
$this->logger->info(
'[HubSpot] Deal is not attached to a pipeline',
[
'pipeline' => $pipelineId]
);
}
$this->cachedBusinessProcesses[$cacheKey] = $businessProcess;
return $businessProcess;
}
private function getBusinessProcess(string $pipelineId): ?BusinessProcess
{
return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);
}
private function getBusinessProcessCacheKey(string $pipelineId): string
{
return $this->config->getId() . '_' . $pipelineId;
}
private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage
{
if (empty($stageId)) {
return null;
}
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;
if (isset($this->cachedStages[$cacheKey])) {
return $this->cachedStages[$cacheKey];
}
$stage = $this->crmEntityRepository->getPipelineStageByConditions(
$businessProcess,
[
'crm_provider_id' => $stageId,
'type' => Stage::TYPE_OPPORTUNITY,
]
);
if ($stage === null) {
$this->importStages(null, $stageId);
}
if ($stage === null) {
$this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);
}
$this->cachedStages[$cacheKey] = $stage;
return $stage;
}
private function resolveAmount(array $properties): ?string
{
$amount = null;
if (! empty($properties['amount'])) {
$amount = str_replace(',', '', $properties['amount']);
}
if ($this->config->hasDefaultCurrencyFieldSet()) {
$valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();
$amount = $properties[$valueFieldName] ?? $amount;
}
return $amount;
}
private function parseCleanDatetime(string $datetime): ?Carbon
{
// Treat pre-1980 values as invalid
$minValidDate = Carbon::parse('1980-01-01 00:00:00');
try {
$date = Carbon::parse($datetime);
if ($minValidDate->gt($date)) {
return null;
}
return $date;
} catch (Exception) {
return null; // On parse error, treat as null
}
}
private function resolveDealProbability(?string $stageProbability): int
{
if ($stageProbability === null) {
return 0;
}
$probability = (float) $stageProbability;
return $probability > 1 ? 0 : (int) ($probability * 100);
}
private function resolveForecastCategory(?string $forecastCategory): string
{
if (! $forecastCategory) {
return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;
}
$forecastCategory = str_replace('_', ' ', $forecastCategory);
return ucwords(strtolower($forecastCategory));
}
private function importExternalFieldData(array $properties, int $opportunityId): void
{
$this->importOpportunityCrmFieldData(
$properties,
$this->getCachedOpportunitySyncableFields(),
$opportunityId
);
}
private function getCachedOpportunitySyncableFields(): array
{
$cacheKey = (string) $this->config->getId();
if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {
$this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();
}
return $this->cachedOpportunitySyncableFields[$cacheKey];
}
private function importOpportunityContacts(Opportunity $opportunity, array $associations): void
{
// Handle empty or missing contact associations
if (empty($associations)) {
// Remove all existing contact associations if none provided
$this->removeAllOpportunityContacts($opportunity);
return;
}
// Use differential sync approach for better performance and accuracy
$this->syncOpportunityContactsDifferential($opportunity, $associations);
}
/**
* Sync opportunity contacts using differential approach
* This compares current vs new associations and only makes necessary changes
*/
private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void
{
$currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);
$contactAssociationIds = array_keys($contactAssociations);
$contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);
$contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);
if (empty($contactsToAdd) && empty($contactsToRemove)) {
return;
}
$this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);
$this->removeContactAssociations($opportunity, $contactsToRemove);
$this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);
}
private function getCurrentContactCrmIds(Opportunity $opportunity): array
{
return $opportunity->contacts()
->pluck('contacts.crm_provider_id')
->toArray();
}
private function logContactAssociationChanges(
Opportunity $opportunity,
array $currentContactCrmIds,
array $contactAssociations,
array $contactsToAdd,
array $contactsToRemove
): void {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [
'opportunity_id' => $opportunity->getId(),
'current_contacts' => $currentContactCrmIds,
'new_contacts' => $contactAssociations,
'contacts_to_add' => $contactsToAdd,
'contacts_to_remove' => $contactsToRemove,
]);
}
private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void
{
if (empty($contactsToRemove)) {
return;
}
$contactsToDetach = $opportunity->contacts()
->whereIn('contacts.crm_provider_id', $contactsToRemove)
->pluck('contacts.id')
->toArray();
if (! empty($contactsToDetach)) {
$opportunity->contacts()->detach($contactsToDetach);
$this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_contact_crm_ids' => $contactsToRemove,
'removed_contact_count' => count($contactsToDetach),
]);
}
}
private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void
{
if (empty($contactsToAdd)) {
return;
}
$contactsAdded = [];
foreach ($contactsToAdd as $crmId) {
$id = $contactAssociations[$crmId];
if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {
$contactsAdded[] = $crmId;
}
}
$this->logAddedContacts($opportunity, $contactsAdded);
}
private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool
{
try {
return $this->performContactAttachment($opportunity, $id, $crmId);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [
'opportunity_id' => $opportunity->getId(),
'contact_crm_id' => $crmId,
'error' => $e->getMessage(),
]);
return false;
}
}
private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool
{
try {
$opportunity->contacts()->attach($contactId, [
'crm_provider_id' => $crmId,
]);
return true;
} catch (\Illuminate\Database\QueryException $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [
'contact_id' => $contactId,
'contact_crm_id' => $crmId,
'opportunity_id' => $opportunity->getId(),
]);
return false;
}
throw $e;
}
}
private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void
{
if (! empty($contactsAdded)) {
$this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [
'opportunity_id' => $opportunity->getId(),
'added_contact_crm_ids' => $contactsAdded,
'added_contacts_count' => count($contactsAdded),
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"32","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"22","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\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseAssociatedId;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Account;\nuse Exception;\nuse Jiminny\\Component\\DealInsights\\Forecast\\Forecast;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Models\\Opportunity;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DealFieldsService;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\HubspotSingleSyncStrategy;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Utils\\CurrencyFormatter;\n\n/**\n * Optimized sync methods for better performance\n * These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains\n */\ntrait OpportunitySyncTrait\n{\n private const int BATCH_SIZE = 100;\n private const int BATCH_PROCESS_SIZE = 800;\n\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected DealFieldsService $dealFieldsService;\n\n private ?array $cachedClosedDealStages = null;\n private array $cachedBusinessProcesses = [];\n private array $cachedStages = [];\n /** @var array<string, array<string>> keyed by config id */\n private array $cachedOpportunitySyncableFields = [];\n /** @var array<string, mixed> keyed by configId:ownerId */\n private array $cachedOwnerProfiles = [];\n /** @var array<string, mixed> keyed by configId:businessProcessId */\n private array $cachedRecordTypes = [];\n\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $startTime = microtime(true);\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n $parameters['config'] = $this->config;\n $syncCount = 0;\n $reportedTotal = 0;\n $lastSyncedId = [];\n $strategyNames = [];\n\n try {\n foreach ($strategies as $strategyName => $syncStrategy) {\n $strategyNames[] = $strategyName;\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,\n ['team' => $this->team->getId()]\n );\n\n $total = 0;\n $lastId = null;\n $buffer = [];\n\n // HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies\n foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {\n $buffer[] = $hsOpportunity;\n\n // process every 800 rows (fits < 1 000 association limit)\n if (\\count($buffer) >= self::BATCH_PROCESS_SIZE) {\n $syncCount += $this->processOpportunityBatch($buffer);\n $buffer = [];\n }\n }\n\n // leftovers\n if ($buffer) {\n $syncCount += $this->processOpportunityBatch($buffer);\n }\n\n $reportedTotal += $total;\n $lastSyncedId = $lastId;\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException | CrmException $e) {\n $this->handleSyncException($e, $parameters);\n }\n\n $durationMs = round((microtime(true) - $startTime) * 1000, 2);\n $this->logger->info(\n '[HubSpot] Synced opportunities',\n [\n 'team' => $this->team->getId(),\n 'strategies' => implode(',', $strategyNames),\n 'sync_count' => $syncCount,\n 'total' => $reportedTotal,\n 'last_synced_id' => $lastSyncedId,\n 'duration_ms' => $durationMs,\n ]\n );\n\n return $reportedTotal;\n }\n\n private function handleSyncException(\\Throwable $e, array $parameters): void\n {\n if (($parameters['since'] ?? null) instanceof Carbon) {\n $parameters['since'] = $parameters['since']->toDateTimeString();\n }\n $parameters['config'] = $this->config->getId();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [\n 'teamId' => $this->team->getUuid(),\n 'parameters' => $parameters,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = [\n 'config' => $this->config,\n 'crm_id' => $crmId,\n ];\n\n try {\n if (! $strategy instanceof HubspotSingleSyncStrategy) {\n throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');\n }\n\n $hsOpportunity = $strategy->fetchOpportunity($parameters);\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);\n\n return $this->importOrUpdateOpportunity($hsOpportunity);\n }\n\n /**\n * Process webhook-collected opportunity batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportOpportunityBatch jobs for batch processing.\n *\n * @return int Number of opportunity IDs dispatched to jobs\n */\n public function batchSyncOpportunities(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n }\n\n /**\n * Import a batch of opportunities by their CRM IDs.\n * Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().\n *\n * @param array<string> $crmIds HubSpot deal CRM IDs\n *\n * @return array{success: array, failed_ids: array, errors?: array<string, string>}\n */\n public function importOpportunityBatchByIds(array $crmIds): array\n {\n $fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);\n\n $allDeals = [];\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n $deals = $this->client->getOpportunitiesByIds($chunk, $fields);\n foreach ($deals as $deal) {\n $allDeals[] = $deal;\n }\n }\n\n // IDs not returned by HubSpot are likely deleted or inaccessible deals.\n // These are not failures — retrying won't bring them back.\n $fetchedIds = array_map('strval', array_column($allDeals, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allDeals),\n ]);\n }\n\n if (empty($allDeals)) {\n return ['success' => [], 'failed_ids' => []];\n }\n\n return $this->importOpportunityBatch($allDeals);\n }\n\n private function getClosedDealStages(): array\n {\n if ($this->cachedClosedDealStages !== null) {\n return $this->cachedClosedDealStages;\n }\n\n $stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);\n $data = [\n 'lost' => [],\n 'won' => [],\n ];\n\n foreach ($stages as $stage) {\n if ($stage->probability == 0.00) {\n $data['lost'][] = $stage->crm_provider_id;\n }\n if ($stage->probability == 100.00) {\n $data['won'][] = $stage->crm_provider_id;\n }\n }\n\n $this->cachedClosedDealStages = $data;\n\n return $data;\n }\n\n /**\n * Import deals into the database with pre-fetched associations.\n *\n * API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT\n * caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()\n * where Laravel retries the whole job with backoff. After all retries exhausted,\n * failed() requeues all IDs to Redis.\n *\n * The per-deal loop catches exceptions individually. A deal can end up in three states:\n * - success: imported/updated successfully\n * - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)\n * These are permanent issues — retrying won't fix them.\n * - skipped (null): missing dependencies (no account, unknown pipeline/stage).\n * This is acceptable — the deal cannot be imported until those exist.\n */\n private function importOpportunityBatch(array $deals): array\n {\n $syncedOpportunities = [\n 'success' => [],\n 'failed_ids' => [],\n ];\n $dealIds = array_column($deals, 'id');\n $batchStart = microtime(true);\n $slowDeals = [];\n\n // Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the\n // queue job retries the whole batch and eventually requeues all deal IDs back to Redis.\n try {\n $companyAssocStart = microtime(true);\n $companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');\n $companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);\n\n $contactAssocStart = microtime(true);\n $contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');\n $contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);\n\n $prepareStart = microtime(true);\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n $prepareTimings = [];\n $associationsData = $this->prepareAssociatedEntities(\n $companyAssociations,\n $contactAssociations,\n $prepareTimings\n );\n $prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $missingCompanies = count(array_diff(\n $allCompanyIds,\n array_keys($associationsData['company_id_mappings'] ?? [])\n ));\n $missingContacts = count(array_diff(\n $allContactIds,\n array_keys($associationsData['contact_id_mappings'] ?? [])\n ));\n\n $existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(\n $this->config,\n array_map('strval', $dealIds)\n );\n $existingCrmIdSet = array_flip($existingCrmIds);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [\n 'teamId' => $this->team->getId(),\n 'dealCount' => count($dealIds),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n $loopStart = microtime(true);\n foreach ($deals as $deal) {\n $dealStart = microtime(true);\n\n try {\n $deal['associations'] = $this->prepareAssociationsForOpportunity(\n $deal['id'],\n $companyAssociations,\n $contactAssociations,\n $associationsData\n );\n\n $syncedOpportunity = $this->importOrUpdateOpportunity(\n $deal,\n isset($existingCrmIdSet[(string) $deal['id']])\n );\n if ($syncedOpportunity) {\n $syncedOpportunities['success'][] = $syncedOpportunity;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $deal['id'],\n 'error' => $e->getMessage(),\n ]);\n $syncedOpportunities['failed_ids'][] = $deal['id'];\n $syncedOpportunities['errors'][$deal['id']] = $e->getMessage();\n }\n\n $dealMs = (int) round((microtime(true) - $dealStart) * 1000);\n if ($dealMs > 1000) {\n $slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [\n 'teamId' => $this->team->getId(),\n 'deal_count' => count($deals),\n 'total_ms' => $totalMs,\n 'company_assoc_api_ms' => $companyAssocMs,\n 'contact_assoc_api_ms' => $contactAssocMs,\n 'prepare_entities_ms' => $prepareMs,\n 'prepare_accounts_ms' => $prepareTimings['accounts_ms'],\n 'prepare_contacts_ms' => $prepareTimings['contacts_ms'],\n 'total_companies' => count($allCompanyIds),\n 'missing_companies' => $missingCompanies,\n 'total_contacts' => count($allContactIds),\n 'missing_contacts' => $missingContacts,\n 'deals_loop_ms' => $loopMs,\n 'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,\n 'slow_deals_count' => count($slowDeals),\n 'slow_deals' => array_slice($slowDeals, 0, 10),\n ]);\n\n return $syncedOpportunities;\n }\n\n /**\n * Prepare associated entities for opportunities with optimized batch processing\n * Returns structured data with CRM ID to DB ID mappings for each opportunity\n */\n private function prepareAssociatedEntities(\n array $companyAssociations,\n array $contactAssociations,\n array &$timings = []\n ): array {\n // Step 1: Collect all unique company and contact IDs from associations\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n\n // Step 2: Batch sync missing entities and get CRM ID to DB ID mappings\n $companyIdMappings = [];\n $contactIdMappings = [];\n $accountsMs = 0;\n $contactsMs = 0;\n\n if (! empty($allCompanyIds)) {\n $start = microtime(true);\n $companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);\n $accountsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n if (! empty($allContactIds)) {\n $start = microtime(true);\n $contactIdMappings = $this->prepareAssociatedContacts($allContactIds);\n $contactsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n $timings = [\n 'accounts_ms' => $accountsMs,\n 'contacts_ms' => $contactsMs,\n ];\n\n return [\n 'company_id_mappings' => $companyIdMappings,\n 'contact_id_mappings' => $contactIdMappings,\n ];\n }\n\n /**\n * Flatten association data to get unique IDs\n */\n private function flattenAssociationIds(array $associations): array\n {\n $ids = [];\n foreach ($associations as $dealAssociations) {\n if (is_array($dealAssociations)) {\n foreach ($dealAssociations as $id) {\n $ids[$id] = true;\n }\n }\n }\n\n return array_keys($ids);\n }\n\n /**\n * Batch sync missing accounts\n */\n private function prepareAssociatedAccounts(array $companyIds): array\n {\n // Find which accounts already exist (lean covering-index lookup)\n $existingAccountsData = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));\n\n if (empty($missingCompanyIds)) {\n return $existingAccountsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [\n 'teamId' => $this->team->getUuid(),\n 'total_companies' => count($companyIds),\n 'existing_companies' => count($existingAccountsData),\n 'missing_companies' => count($missingCompanyIds),\n ]);\n\n // we already have limit on opportunity ids count\n // Initialize variable before try block\n $syncedAccountsData = [];\n\n try {\n $syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [\n 'size' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedAccountsData = [];\n }\n\n return $existingAccountsData + $syncedAccountsData;\n }\n\n /**\n * Prepare associated contacts - find existing and sync missing ones\n * Returns mapping of CRM ID to DB ID\n */\n private function prepareAssociatedContacts(array $contactIds): array\n {\n // Find which contacts already exist (lean covering-index lookup)\n $existingContactsData = $this->crmEntityRepository\n ->getExistingContactIdsMap($this->config, $contactIds);\n\n $missingContactIds = array_diff($contactIds, array_keys($existingContactsData));\n\n if (empty($missingContactIds)) {\n return $existingContactsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [\n 'teamId' => $this->team->getUuid(),\n 'total_contacts' => count($contactIds),\n 'existing_contacts' => count($existingContactsData),\n 'missing_contacts' => count($missingContactIds),\n ]);\n\n // Sync missing contacts using batch API\n try {\n $syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [\n 'size' => count($missingContactIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedContactsData = [];\n }\n\n return $existingContactsData + $syncedContactsData;\n }\n\n private function batchSyncCrmObjects(string $objectType, array $crmIds): array\n {\n $syncObjects = [];\n $crmObjectIds = array_values($crmIds);\n\n foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {\n try {\n $objects = $objectType === 'companies' ?\n $this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :\n $this->client->getContactsByIds($chunk, $this->getContactFields());\n\n foreach ($objects as $objectId => $objectData) {\n $this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [\n 'requested_count' => count($chunk),\n 'synced_count' => count($objects),\n ]);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [\n 'ids' => $chunk,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncObjects;\n }\n\n private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void\n {\n try {\n $object = $objectType === 'companies' ?\n $this->importAccount($objectData) :\n $this->importContact($objectData);\n\n if ($object) {\n $syncObjects[$object->getCrmProviderId()] = $object->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [\n 'id' => $objectId,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n /**\n * Prepare associations for a single opportunity\n *\n * The return value is an array with the following structure:\n * [\n * 'companies' => [\n * $companyCrmId => $companyId,\n * ...\n * ],\n * 'contacts' => [\n * $contactCrmId => $contactId,\n * ...\n * ],\n * 'account_id' => $accountId,\n * ]\n */\n private function prepareAssociationsForOpportunity(\n string $oppCrmId,\n array $companyAssociations,\n array $contactAssociations,\n array $associationsData\n ): array {\n $associations = [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n\n $oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];\n foreach ($oppCompanyIds as $companyCrmId) {\n if (isset($associationsData['company_id_mappings'][$companyCrmId])) {\n $associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];\n\n // Set primary account (first company becomes primary account)\n if ($associations['account_id'] === null) {\n $associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];\n }\n }\n }\n\n $oppContactIds = $contactAssociations[$oppCrmId] ?? [];\n foreach ($oppContactIds as $contactCrmId) {\n if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {\n $associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];\n }\n }\n\n return $associations;\n }\n\n /**\n * Update only associations for an opportunity\n */\n private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void\n {\n // Update contact associations\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n // Update company (account) associations\n $this->updateOpportunityAccount($opportunity, $associations['account_id']);\n }\n\n /**\n * Remove all contact associations from an opportunity\n */\n private function removeAllOpportunityContacts(Opportunity $opportunity): void\n {\n $currentCount = (int) $opportunity->contacts()->count();\n\n if ($currentCount > 0) {\n $opportunity->contacts()->detach();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_count' => $currentCount,\n ]);\n }\n }\n\n private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void\n {\n if ($accountId === null) {\n // No account ID provided - keep current account\n return;\n }\n\n $currentAccountId = $opportunity->getAccountId();\n\n // Only update if account has changed\n if ($currentAccountId !== $accountId) {\n $opportunity->account_id = $accountId;\n $opportunity->save();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [\n 'opportunity_id' => $opportunity->getId(),\n 'old_account_id' => $currentAccountId,\n 'new_account_id' => $accountId,\n ]);\n }\n }\n\n /**\n * Find existing opportunities by external IDs (OPTIMIZED VERSION)\n * Uses batch query for better performance\n */\n private function findExistingOpportunities(array $crmIds): Collection\n {\n return $this->crmEntityRepository\n ->findOpportunitiesByExternalIds($this->config, $crmIds);\n }\n\n private function processOpportunityBatch(array $opportunities): int\n {\n $syncedOpportunities = $this->importOpportunityBatch($opportunities);\n\n return count($syncedOpportunities['success'] ?? []);\n }\n\n /**\n * Convert single deal associations from HubSpot format to internal format\n * Handles both HubSpot SDK objects and array formats\n *\n * @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed\n *\n * @return array Processed associations with DB IDs\n */\n private function convertDealAssociations(array $opportunityAssociations): array\n {\n $associations = $this->initializeAssociationsStructure();\n\n if (empty($opportunityAssociations)) {\n return $associations;\n }\n\n $associationIds = $this->extractAssociationIds($opportunityAssociations);\n\n $this->processCompanyAssociations($associationIds, $associations);\n $this->processContactAssociations($associationIds, $associations);\n\n return $associations;\n }\n\n private function initializeAssociationsStructure(): array\n {\n return [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n }\n\n private function extractAssociationIds(array $opportunityAssociations): array\n {\n $associationIds = [];\n\n foreach ($opportunityAssociations as $type => $associationData) {\n if (! empty($associationData)) {\n $associationIds[$type] = $this->convertSingleDealAssociations($associationData);\n }\n }\n\n return $associationIds;\n }\n\n private function processCompanyAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['companies'])) {\n return;\n }\n\n $companyId = $associationIds['companies'][0];\n $account = $this->findOrSyncAccount($companyId);\n\n if ($account instanceof Account) {\n $associations['companies'][$companyId] = $account->getId();\n $associations['account_id'] = $account->getId();\n }\n }\n\n private function processContactAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['contacts'])) {\n return;\n }\n\n foreach ($associationIds['contacts'] as $contactId) {\n $contact = $this->findOrSyncContact($contactId);\n\n if ($contact instanceof Contact) {\n $associations['contacts'][$contactId] = $contact->getId();\n }\n }\n }\n\n private function findOrSyncAccount(string $companyId): ?Account\n {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);\n\n if (! $account instanceof Account) {\n $account = $this->syncAccount($companyId);\n }\n\n return $account;\n }\n\n private function findOrSyncContact(string $contactId): ?Contact\n {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);\n\n if (! $contact instanceof Contact) {\n $contact = $this->syncContact($contactId);\n }\n\n return $contact;\n }\n\n private function convertSingleDealAssociations($opportunityAssociations = null): array\n {\n $associationData = [];\n\n if ($opportunityAssociations === null) {\n return $associationData;\n }\n\n // Handle array input (from extractAssociationIds)\n if (is_array($opportunityAssociations)) {\n return $opportunityAssociations;\n }\n\n // Handle CollectionResponseAssociatedId object\n if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {\n foreach ($opportunityAssociations->getResults() as $association) {\n $associationData[] = $association->getId();\n }\n }\n\n return $associationData;\n }\n\n private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity\n {\n if (empty($crmData['properties'])) {\n return null;\n }\n\n $crmId = (string) $crmData['id'];\n $properties = $crmData['properties'];\n $associations = $crmData['associations'] ?? [];\n\n $opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(\n $this->config,\n $crmId\n );\n\n if ($opportunityExists) {\n return $this->updateOpportunity($crmId, $properties, $associations);\n }\n\n return $this->createOpportunity($crmId, $properties, $associations);\n }\n\n /**\n * Create new opportunity\n */\n private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n if (! $accountId) {\n return null;\n }\n\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n if (! $businessProcess) {\n return null;\n }\n\n $stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);\n if (! $stage) {\n return null;\n }\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n }\n\n /**\n * Update existing opportunity\n */\n private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n $stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->updateOpportunityAssociations($opportunity, $associations);\n\n return $opportunity;\n }\n\n private function resolveAccountId(array $associations): ?int\n {\n if (! empty($associations['account_id'])) {\n return $associations['account_id'];\n }\n\n if (empty($associations)) {\n return null;\n }\n\n // Fallback: use first company as account (currently SDK returns one company)\n foreach ($associations['companies'] as $accountId) {\n return $accountId;\n }\n\n return null;\n }\n\n private function buildOpportunityData(\n array $properties,\n ?int $accountId,\n ?BusinessProcess $businessProcess,\n ?Stage $stage\n ): array {\n $ownerId = null;\n $profile = null;\n if (! empty($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->getCachedOwnerProfile((string) $ownerId);\n }\n\n $name = 'Unknown';\n if (isset($properties['dealname'])) {\n $name = mb_strimwidth($properties['dealname'], 0, 128);\n }\n\n $amount = $this->resolveAmount($properties);\n $currency = $properties['deal_currency_code'] ?? null;\n\n $closeDate = null;\n if (! empty($properties['closedate'])) {\n $closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');\n }\n\n $remotelyCreatedAt = null;\n if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {\n $date = $this->parseCleanDatetime($properties['createdate']);\n $remotelyCreatedAt = $date?->format('Y-m-d H:i:s');\n }\n\n $closedStages = $this->getClosedDealStages();\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $data = [\n 'team_id' => $this->team->getId(),\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'value' => ! empty($amount) ? $amount : null,\n 'currency_code' => CurrencyFormatter::formatCode($currency),\n 'close_date' => $closeDate,\n 'is_closed' => $isWon || $isLost,\n 'is_won' => $isWon,\n 'remotely_created_at' => $remotelyCreatedAt,\n 'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),\n 'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),\n ];\n\n if ($accountId) {\n $data['account_id'] = $accountId;\n }\n\n if ($stage) {\n $data['stage_id'] = $stage->id;\n }\n\n if ($businessProcess) {\n $recordType = $this->getCachedBusinessProcessRecordType($businessProcess);\n if ($recordType) {\n $data['record_type_id'] = $recordType->id;\n }\n }\n\n return $data;\n }\n\n private function getCachedOwnerProfile(string $ownerId): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $ownerId;\n if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {\n return $this->cachedOwnerProfiles[$cacheKey];\n }\n\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);\n $this->cachedOwnerProfiles[$cacheKey] = $profile;\n\n return $profile;\n }\n\n private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId();\n if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {\n return $this->cachedRecordTypes[$cacheKey];\n }\n\n $recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);\n $this->cachedRecordTypes[$cacheKey] = $recordType;\n\n return $recordType;\n }\n\n private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess\n {\n if ($pipelineId === null) {\n return null;\n }\n\n $cacheKey = $this->getBusinessProcessCacheKey($pipelineId);\n if (isset($this->cachedBusinessProcesses[$cacheKey])) {\n return $this->cachedBusinessProcesses[$cacheKey];\n }\n\n $businessProcess = $this->getBusinessProcess($pipelineId);\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->importStages();\n $businessProcess = $this->getBusinessProcess($pipelineId);\n }\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->logger->info(\n '[HubSpot] Deal is not attached to a pipeline',\n [\n 'pipeline' => $pipelineId]\n );\n }\n\n $this->cachedBusinessProcesses[$cacheKey] = $businessProcess;\n\n return $businessProcess;\n }\n\n private function getBusinessProcess(string $pipelineId): ?BusinessProcess\n {\n return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);\n }\n\n private function getBusinessProcessCacheKey(string $pipelineId): string\n {\n return $this->config->getId() . '_' . $pipelineId;\n }\n\n private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage\n {\n if (empty($stageId)) {\n return null;\n }\n\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;\n if (isset($this->cachedStages[$cacheKey])) {\n return $this->cachedStages[$cacheKey];\n }\n\n $stage = $this->crmEntityRepository->getPipelineStageByConditions(\n $businessProcess,\n [\n 'crm_provider_id' => $stageId,\n 'type' => Stage::TYPE_OPPORTUNITY,\n ]\n );\n\n if ($stage === null) {\n $this->importStages(null, $stageId);\n }\n\n if ($stage === null) {\n $this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);\n }\n\n $this->cachedStages[$cacheKey] = $stage;\n\n return $stage;\n }\n\n private function resolveAmount(array $properties): ?string\n {\n $amount = null;\n if (! empty($properties['amount'])) {\n $amount = str_replace(',', '', $properties['amount']);\n }\n\n if ($this->config->hasDefaultCurrencyFieldSet()) {\n $valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();\n $amount = $properties[$valueFieldName] ?? $amount;\n }\n\n return $amount;\n }\n\n private function parseCleanDatetime(string $datetime): ?Carbon\n {\n // Treat pre-1980 values as invalid\n $minValidDate = Carbon::parse('1980-01-01 00:00:00');\n\n try {\n $date = Carbon::parse($datetime);\n\n if ($minValidDate->gt($date)) {\n return null;\n }\n\n return $date;\n } catch (Exception) {\n return null; // On parse error, treat as null\n }\n }\n\n private function resolveDealProbability(?string $stageProbability): int\n {\n if ($stageProbability === null) {\n return 0;\n }\n\n $probability = (float) $stageProbability;\n\n return $probability > 1 ? 0 : (int) ($probability * 100);\n }\n\n private function resolveForecastCategory(?string $forecastCategory): string\n {\n if (! $forecastCategory) {\n return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;\n }\n\n $forecastCategory = str_replace('_', ' ', $forecastCategory);\n\n return ucwords(strtolower($forecastCategory));\n }\n\n private function importExternalFieldData(array $properties, int $opportunityId): void\n {\n $this->importOpportunityCrmFieldData(\n $properties,\n $this->getCachedOpportunitySyncableFields(),\n $opportunityId\n );\n }\n\n private function getCachedOpportunitySyncableFields(): array\n {\n $cacheKey = (string) $this->config->getId();\n if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {\n $this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();\n }\n\n return $this->cachedOpportunitySyncableFields[$cacheKey];\n }\n\n private function importOpportunityContacts(Opportunity $opportunity, array $associations): void\n {\n // Handle empty or missing contact associations\n if (empty($associations)) {\n // Remove all existing contact associations if none provided\n $this->removeAllOpportunityContacts($opportunity);\n\n return;\n }\n\n // Use differential sync approach for better performance and accuracy\n $this->syncOpportunityContactsDifferential($opportunity, $associations);\n }\n\n /**\n * Sync opportunity contacts using differential approach\n * This compares current vs new associations and only makes necessary changes\n */\n private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void\n {\n $currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);\n $contactAssociationIds = array_keys($contactAssociations);\n\n $contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);\n $contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);\n\n if (empty($contactsToAdd) && empty($contactsToRemove)) {\n return;\n }\n\n $this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);\n\n $this->removeContactAssociations($opportunity, $contactsToRemove);\n $this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);\n }\n\n private function getCurrentContactCrmIds(Opportunity $opportunity): array\n {\n return $opportunity->contacts()\n ->pluck('contacts.crm_provider_id')\n ->toArray();\n }\n\n private function logContactAssociationChanges(\n Opportunity $opportunity,\n array $currentContactCrmIds,\n array $contactAssociations,\n array $contactsToAdd,\n array $contactsToRemove\n ): void {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [\n 'opportunity_id' => $opportunity->getId(),\n 'current_contacts' => $currentContactCrmIds,\n 'new_contacts' => $contactAssociations,\n 'contacts_to_add' => $contactsToAdd,\n 'contacts_to_remove' => $contactsToRemove,\n ]);\n }\n\n private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void\n {\n if (empty($contactsToRemove)) {\n return;\n }\n\n $contactsToDetach = $opportunity->contacts()\n ->whereIn('contacts.crm_provider_id', $contactsToRemove)\n ->pluck('contacts.id')\n ->toArray();\n\n if (! empty($contactsToDetach)) {\n $opportunity->contacts()->detach($contactsToDetach);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_contact_crm_ids' => $contactsToRemove,\n 'removed_contact_count' => count($contactsToDetach),\n ]);\n }\n }\n\n private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void\n {\n if (empty($contactsToAdd)) {\n return;\n }\n\n $contactsAdded = [];\n foreach ($contactsToAdd as $crmId) {\n $id = $contactAssociations[$crmId];\n\n if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {\n $contactsAdded[] = $crmId;\n }\n }\n\n $this->logAddedContacts($opportunity, $contactsAdded);\n }\n\n private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool\n {\n try {\n return $this->performContactAttachment($opportunity, $id, $crmId);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [\n 'opportunity_id' => $opportunity->getId(),\n 'contact_crm_id' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool\n {\n try {\n $opportunity->contacts()->attach($contactId, [\n 'crm_provider_id' => $crmId,\n ]);\n\n return true;\n } catch (\\Illuminate\\Database\\QueryException $e) {\n if (str_contains($e->getMessage(), 'Duplicate entry')) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [\n 'contact_id' => $contactId,\n 'contact_crm_id' => $crmId,\n 'opportunity_id' => $opportunity->getId(),\n ]);\n\n return false;\n }\n\n throw $e;\n }\n }\n\n private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void\n {\n if (! empty($contactsAdded)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'added_contact_crm_ids' => $contactsAdded,\n 'added_contacts_count' => count($contactsAdded),\n ]);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseAssociatedId;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Models\\Account;\nuse Exception;\nuse Jiminny\\Component\\DealInsights\\Forecast\\Forecast;\nuse Jiminny\\Jobs\\Crm\\MatchActivitiesToNewOpportunity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Models\\Opportunity;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Crm\\Hubspot\\DealFieldsService;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\HubspotSingleSyncStrategy;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Utils\\CurrencyFormatter;\n\n/**\n * Optimized sync methods for better performance\n * These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains\n */\ntrait OpportunitySyncTrait\n{\n private const int BATCH_SIZE = 100;\n private const int BATCH_PROCESS_SIZE = 800;\n\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected DealFieldsService $dealFieldsService;\n\n private ?array $cachedClosedDealStages = null;\n private array $cachedBusinessProcesses = [];\n private array $cachedStages = [];\n /** @var array<string, array<string>> keyed by config id */\n private array $cachedOpportunitySyncableFields = [];\n /** @var array<string, mixed> keyed by configId:ownerId */\n private array $cachedOwnerProfiles = [];\n /** @var array<string, mixed> keyed by configId:businessProcessId */\n private array $cachedRecordTypes = [];\n\n public function syncOpportunities(array $parameters, ?string $strategy = null): int\n {\n $startTime = microtime(true);\n $strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);\n $parameters['config'] = $this->config;\n $syncCount = 0;\n $reportedTotal = 0;\n $lastSyncedId = [];\n $strategyNames = [];\n\n try {\n foreach ($strategies as $strategyName => $syncStrategy) {\n $strategyNames[] = $strategyName;\n $this->logger->info(\n '[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,\n ['team' => $this->team->getId()]\n );\n\n $total = 0;\n $lastId = null;\n $buffer = [];\n\n // HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies\n foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {\n $buffer[] = $hsOpportunity;\n\n // process every 800 rows (fits < 1 000 association limit)\n if (\\count($buffer) >= self::BATCH_PROCESS_SIZE) {\n $syncCount += $this->processOpportunityBatch($buffer);\n $buffer = [];\n }\n }\n\n // leftovers\n if ($buffer) {\n $syncCount += $this->processOpportunityBatch($buffer);\n }\n\n $reportedTotal += $total;\n $lastSyncedId = $lastId;\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException | CrmException $e) {\n $this->handleSyncException($e, $parameters);\n }\n\n $durationMs = round((microtime(true) - $startTime) * 1000, 2);\n $this->logger->info(\n '[HubSpot] Synced opportunities',\n [\n 'team' => $this->team->getId(),\n 'strategies' => implode(',', $strategyNames),\n 'sync_count' => $syncCount,\n 'total' => $reportedTotal,\n 'last_synced_id' => $lastSyncedId,\n 'duration_ms' => $durationMs,\n ]\n );\n\n return $reportedTotal;\n }\n\n private function handleSyncException(\\Throwable $e, array $parameters): void\n {\n if (($parameters['since'] ?? null) instanceof Carbon) {\n $parameters['since'] = $parameters['since']->toDateTimeString();\n }\n $parameters['config'] = $this->config->getId();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [\n 'teamId' => $this->team->getUuid(),\n 'parameters' => $parameters,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n /**\n * @inheritdoc\n */\n public function syncOpportunity(string $crmId): ?Opportunity\n {\n $strategy = $this->opportunitySyncStrategyResolver->resolve(\n $this->config,\n OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,\n );\n\n $parameters = [\n 'config' => $this->config,\n 'crm_id' => $crmId,\n ];\n\n try {\n if (! $strategy instanceof HubspotSingleSyncStrategy) {\n throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');\n }\n\n $hsOpportunity = $strategy->fetchOpportunity($parameters);\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n $hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);\n\n return $this->importOrUpdateOpportunity($hsOpportunity);\n }\n\n /**\n * Process webhook-collected opportunity batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportOpportunityBatch jobs for batch processing.\n *\n * @return int Number of opportunity IDs dispatched to jobs\n */\n public function batchSyncOpportunities(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n }\n\n /**\n * Import a batch of opportunities by their CRM IDs.\n * Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().\n *\n * @param array<string> $crmIds HubSpot deal CRM IDs\n *\n * @return array{success: array, failed_ids: array, errors?: array<string, string>}\n */\n public function importOpportunityBatchByIds(array $crmIds): array\n {\n $fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);\n\n $allDeals = [];\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n $deals = $this->client->getOpportunitiesByIds($chunk, $fields);\n foreach ($deals as $deal) {\n $allDeals[] = $deal;\n }\n }\n\n // IDs not returned by HubSpot are likely deleted or inaccessible deals.\n // These are not failures — retrying won't bring them back.\n $fetchedIds = array_map('strval', array_column($allDeals, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allDeals),\n ]);\n }\n\n if (empty($allDeals)) {\n return ['success' => [], 'failed_ids' => []];\n }\n\n return $this->importOpportunityBatch($allDeals);\n }\n\n private function getClosedDealStages(): array\n {\n if ($this->cachedClosedDealStages !== null) {\n return $this->cachedClosedDealStages;\n }\n\n $stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);\n $data = [\n 'lost' => [],\n 'won' => [],\n ];\n\n foreach ($stages as $stage) {\n if ($stage->probability == 0.00) {\n $data['lost'][] = $stage->crm_provider_id;\n }\n if ($stage->probability == 100.00) {\n $data['won'][] = $stage->crm_provider_id;\n }\n }\n\n $this->cachedClosedDealStages = $data;\n\n return $data;\n }\n\n /**\n * Import deals into the database with pre-fetched associations.\n *\n * API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT\n * caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()\n * where Laravel retries the whole job with backoff. After all retries exhausted,\n * failed() requeues all IDs to Redis.\n *\n * The per-deal loop catches exceptions individually. A deal can end up in three states:\n * - success: imported/updated successfully\n * - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)\n * These are permanent issues — retrying won't fix them.\n * - skipped (null): missing dependencies (no account, unknown pipeline/stage).\n * This is acceptable — the deal cannot be imported until those exist.\n */\n private function importOpportunityBatch(array $deals): array\n {\n $syncedOpportunities = [\n 'success' => [],\n 'failed_ids' => [],\n ];\n $dealIds = array_column($deals, 'id');\n $batchStart = microtime(true);\n $slowDeals = [];\n\n // Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the\n // queue job retries the whole batch and eventually requeues all deal IDs back to Redis.\n try {\n $companyAssocStart = microtime(true);\n $companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');\n $companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);\n\n $contactAssocStart = microtime(true);\n $contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');\n $contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);\n\n $prepareStart = microtime(true);\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n $prepareTimings = [];\n $associationsData = $this->prepareAssociatedEntities(\n $companyAssociations,\n $contactAssociations,\n $prepareTimings\n );\n $prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $missingCompanies = count(array_diff(\n $allCompanyIds,\n array_keys($associationsData['company_id_mappings'] ?? [])\n ));\n $missingContacts = count(array_diff(\n $allContactIds,\n array_keys($associationsData['contact_id_mappings'] ?? [])\n ));\n\n $existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(\n $this->config,\n array_map('strval', $dealIds)\n );\n $existingCrmIdSet = array_flip($existingCrmIds);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [\n 'teamId' => $this->team->getId(),\n 'dealCount' => count($dealIds),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n $loopStart = microtime(true);\n foreach ($deals as $deal) {\n $dealStart = microtime(true);\n\n try {\n $deal['associations'] = $this->prepareAssociationsForOpportunity(\n $deal['id'],\n $companyAssociations,\n $contactAssociations,\n $associationsData\n );\n\n $syncedOpportunity = $this->importOrUpdateOpportunity(\n $deal,\n isset($existingCrmIdSet[(string) $deal['id']])\n );\n if ($syncedOpportunity) {\n $syncedOpportunities['success'][] = $syncedOpportunity;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $deal['id'],\n 'error' => $e->getMessage(),\n ]);\n $syncedOpportunities['failed_ids'][] = $deal['id'];\n $syncedOpportunities['errors'][$deal['id']] = $e->getMessage();\n }\n\n $dealMs = (int) round((microtime(true) - $dealStart) * 1000);\n if ($dealMs > 1000) {\n $slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [\n 'teamId' => $this->team->getId(),\n 'deal_count' => count($deals),\n 'total_ms' => $totalMs,\n 'company_assoc_api_ms' => $companyAssocMs,\n 'contact_assoc_api_ms' => $contactAssocMs,\n 'prepare_entities_ms' => $prepareMs,\n 'prepare_accounts_ms' => $prepareTimings['accounts_ms'],\n 'prepare_contacts_ms' => $prepareTimings['contacts_ms'],\n 'total_companies' => count($allCompanyIds),\n 'missing_companies' => $missingCompanies,\n 'total_contacts' => count($allContactIds),\n 'missing_contacts' => $missingContacts,\n 'deals_loop_ms' => $loopMs,\n 'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,\n 'slow_deals_count' => count($slowDeals),\n 'slow_deals' => array_slice($slowDeals, 0, 10),\n ]);\n\n return $syncedOpportunities;\n }\n\n /**\n * Prepare associated entities for opportunities with optimized batch processing\n * Returns structured data with CRM ID to DB ID mappings for each opportunity\n */\n private function prepareAssociatedEntities(\n array $companyAssociations,\n array $contactAssociations,\n array &$timings = []\n ): array {\n // Step 1: Collect all unique company and contact IDs from associations\n $allCompanyIds = $this->flattenAssociationIds($companyAssociations);\n $allContactIds = $this->flattenAssociationIds($contactAssociations);\n\n // Step 2: Batch sync missing entities and get CRM ID to DB ID mappings\n $companyIdMappings = [];\n $contactIdMappings = [];\n $accountsMs = 0;\n $contactsMs = 0;\n\n if (! empty($allCompanyIds)) {\n $start = microtime(true);\n $companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);\n $accountsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n if (! empty($allContactIds)) {\n $start = microtime(true);\n $contactIdMappings = $this->prepareAssociatedContacts($allContactIds);\n $contactsMs = (int) round((microtime(true) - $start) * 1000);\n }\n\n $timings = [\n 'accounts_ms' => $accountsMs,\n 'contacts_ms' => $contactsMs,\n ];\n\n return [\n 'company_id_mappings' => $companyIdMappings,\n 'contact_id_mappings' => $contactIdMappings,\n ];\n }\n\n /**\n * Flatten association data to get unique IDs\n */\n private function flattenAssociationIds(array $associations): array\n {\n $ids = [];\n foreach ($associations as $dealAssociations) {\n if (is_array($dealAssociations)) {\n foreach ($dealAssociations as $id) {\n $ids[$id] = true;\n }\n }\n }\n\n return array_keys($ids);\n }\n\n /**\n * Batch sync missing accounts\n */\n private function prepareAssociatedAccounts(array $companyIds): array\n {\n // Find which accounts already exist (lean covering-index lookup)\n $existingAccountsData = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));\n\n if (empty($missingCompanyIds)) {\n return $existingAccountsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [\n 'teamId' => $this->team->getUuid(),\n 'total_companies' => count($companyIds),\n 'existing_companies' => count($existingAccountsData),\n 'missing_companies' => count($missingCompanyIds),\n ]);\n\n // we already have limit on opportunity ids count\n // Initialize variable before try block\n $syncedAccountsData = [];\n\n try {\n $syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [\n 'size' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedAccountsData = [];\n }\n\n return $existingAccountsData + $syncedAccountsData;\n }\n\n /**\n * Prepare associated contacts - find existing and sync missing ones\n * Returns mapping of CRM ID to DB ID\n */\n private function prepareAssociatedContacts(array $contactIds): array\n {\n // Find which contacts already exist (lean covering-index lookup)\n $existingContactsData = $this->crmEntityRepository\n ->getExistingContactIdsMap($this->config, $contactIds);\n\n $missingContactIds = array_diff($contactIds, array_keys($existingContactsData));\n\n if (empty($missingContactIds)) {\n return $existingContactsData;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [\n 'teamId' => $this->team->getUuid(),\n 'total_contacts' => count($contactIds),\n 'existing_contacts' => count($existingContactsData),\n 'missing_contacts' => count($missingContactIds),\n ]);\n\n // Sync missing contacts using batch API\n try {\n $syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [\n 'size' => count($missingContactIds),\n 'error' => $e->getMessage(),\n ]);\n $syncedContactsData = [];\n }\n\n return $existingContactsData + $syncedContactsData;\n }\n\n private function batchSyncCrmObjects(string $objectType, array $crmIds): array\n {\n $syncObjects = [];\n $crmObjectIds = array_values($crmIds);\n\n foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {\n try {\n $objects = $objectType === 'companies' ?\n $this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :\n $this->client->getContactsByIds($chunk, $this->getContactFields());\n\n foreach ($objects as $objectId => $objectData) {\n $this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [\n 'requested_count' => count($chunk),\n 'synced_count' => count($objects),\n ]);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [\n 'ids' => $chunk,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncObjects;\n }\n\n private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void\n {\n try {\n $object = $objectType === 'companies' ?\n $this->importAccount($objectData) :\n $this->importContact($objectData);\n\n if ($object) {\n $syncObjects[$object->getCrmProviderId()] = $object->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [\n 'id' => $objectId,\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n /**\n * Prepare associations for a single opportunity\n *\n * The return value is an array with the following structure:\n * [\n * 'companies' => [\n * $companyCrmId => $companyId,\n * ...\n * ],\n * 'contacts' => [\n * $contactCrmId => $contactId,\n * ...\n * ],\n * 'account_id' => $accountId,\n * ]\n */\n private function prepareAssociationsForOpportunity(\n string $oppCrmId,\n array $companyAssociations,\n array $contactAssociations,\n array $associationsData\n ): array {\n $associations = [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n\n $oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];\n foreach ($oppCompanyIds as $companyCrmId) {\n if (isset($associationsData['company_id_mappings'][$companyCrmId])) {\n $associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];\n\n // Set primary account (first company becomes primary account)\n if ($associations['account_id'] === null) {\n $associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];\n }\n }\n }\n\n $oppContactIds = $contactAssociations[$oppCrmId] ?? [];\n foreach ($oppContactIds as $contactCrmId) {\n if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {\n $associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];\n }\n }\n\n return $associations;\n }\n\n /**\n * Update only associations for an opportunity\n */\n private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void\n {\n // Update contact associations\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n // Update company (account) associations\n $this->updateOpportunityAccount($opportunity, $associations['account_id']);\n }\n\n /**\n * Remove all contact associations from an opportunity\n */\n private function removeAllOpportunityContacts(Opportunity $opportunity): void\n {\n $currentCount = (int) $opportunity->contacts()->count();\n\n if ($currentCount > 0) {\n $opportunity->contacts()->detach();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_count' => $currentCount,\n ]);\n }\n }\n\n private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void\n {\n if ($accountId === null) {\n // No account ID provided - keep current account\n return;\n }\n\n $currentAccountId = $opportunity->getAccountId();\n\n // Only update if account has changed\n if ($currentAccountId !== $accountId) {\n $opportunity->account_id = $accountId;\n $opportunity->save();\n\n $this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [\n 'opportunity_id' => $opportunity->getId(),\n 'old_account_id' => $currentAccountId,\n 'new_account_id' => $accountId,\n ]);\n }\n }\n\n /**\n * Find existing opportunities by external IDs (OPTIMIZED VERSION)\n * Uses batch query for better performance\n */\n private function findExistingOpportunities(array $crmIds): Collection\n {\n return $this->crmEntityRepository\n ->findOpportunitiesByExternalIds($this->config, $crmIds);\n }\n\n private function processOpportunityBatch(array $opportunities): int\n {\n $syncedOpportunities = $this->importOpportunityBatch($opportunities);\n\n return count($syncedOpportunities['success'] ?? []);\n }\n\n /**\n * Convert single deal associations from HubSpot format to internal format\n * Handles both HubSpot SDK objects and array formats\n *\n * @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed\n *\n * @return array Processed associations with DB IDs\n */\n private function convertDealAssociations(array $opportunityAssociations): array\n {\n $associations = $this->initializeAssociationsStructure();\n\n if (empty($opportunityAssociations)) {\n return $associations;\n }\n\n $associationIds = $this->extractAssociationIds($opportunityAssociations);\n\n $this->processCompanyAssociations($associationIds, $associations);\n $this->processContactAssociations($associationIds, $associations);\n\n return $associations;\n }\n\n private function initializeAssociationsStructure(): array\n {\n return [\n 'companies' => [],\n 'contacts' => [],\n 'account_id' => null, // Primary account for opportunity\n ];\n }\n\n private function extractAssociationIds(array $opportunityAssociations): array\n {\n $associationIds = [];\n\n foreach ($opportunityAssociations as $type => $associationData) {\n if (! empty($associationData)) {\n $associationIds[$type] = $this->convertSingleDealAssociations($associationData);\n }\n }\n\n return $associationIds;\n }\n\n private function processCompanyAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['companies'])) {\n return;\n }\n\n $companyId = $associationIds['companies'][0];\n $account = $this->findOrSyncAccount($companyId);\n\n if ($account instanceof Account) {\n $associations['companies'][$companyId] = $account->getId();\n $associations['account_id'] = $account->getId();\n }\n }\n\n private function processContactAssociations(array $associationIds, array &$associations): void\n {\n if (empty($associationIds['contacts'])) {\n return;\n }\n\n foreach ($associationIds['contacts'] as $contactId) {\n $contact = $this->findOrSyncContact($contactId);\n\n if ($contact instanceof Contact) {\n $associations['contacts'][$contactId] = $contact->getId();\n }\n }\n }\n\n private function findOrSyncAccount(string $companyId): ?Account\n {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);\n\n if (! $account instanceof Account) {\n $account = $this->syncAccount($companyId);\n }\n\n return $account;\n }\n\n private function findOrSyncContact(string $contactId): ?Contact\n {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);\n\n if (! $contact instanceof Contact) {\n $contact = $this->syncContact($contactId);\n }\n\n return $contact;\n }\n\n private function convertSingleDealAssociations($opportunityAssociations = null): array\n {\n $associationData = [];\n\n if ($opportunityAssociations === null) {\n return $associationData;\n }\n\n // Handle array input (from extractAssociationIds)\n if (is_array($opportunityAssociations)) {\n return $opportunityAssociations;\n }\n\n // Handle CollectionResponseAssociatedId object\n if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {\n foreach ($opportunityAssociations->getResults() as $association) {\n $associationData[] = $association->getId();\n }\n }\n\n return $associationData;\n }\n\n private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity\n {\n if (empty($crmData['properties'])) {\n return null;\n }\n\n $crmId = (string) $crmData['id'];\n $properties = $crmData['properties'];\n $associations = $crmData['associations'] ?? [];\n\n $opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(\n $this->config,\n $crmId\n );\n\n if ($opportunityExists) {\n return $this->updateOpportunity($crmId, $properties, $associations);\n }\n\n return $this->createOpportunity($crmId, $properties, $associations);\n }\n\n /**\n * Create new opportunity\n */\n private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n if (! $accountId) {\n return null;\n }\n\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n if (! $businessProcess) {\n return null;\n }\n\n $stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);\n if (! $stage) {\n return null;\n }\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->importOpportunityContacts($opportunity, $associations['contacts']);\n\n if ($opportunity->wasRecentlyCreated) {\n MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());\n }\n\n return $opportunity;\n }\n\n /**\n * Update existing opportunity\n */\n private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity\n {\n $accountId = $this->resolveAccountId($associations);\n $businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);\n $stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;\n\n $data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);\n\n $attributes = [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $crmId,\n ];\n\n $values = array_merge($attributes, $data);\n $opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);\n\n $this->importExternalFieldData($properties, $opportunity->getId());\n $this->updateOpportunityAssociations($opportunity, $associations);\n\n return $opportunity;\n }\n\n private function resolveAccountId(array $associations): ?int\n {\n if (! empty($associations['account_id'])) {\n return $associations['account_id'];\n }\n\n if (empty($associations)) {\n return null;\n }\n\n // Fallback: use first company as account (currently SDK returns one company)\n foreach ($associations['companies'] as $accountId) {\n return $accountId;\n }\n\n return null;\n }\n\n private function buildOpportunityData(\n array $properties,\n ?int $accountId,\n ?BusinessProcess $businessProcess,\n ?Stage $stage\n ): array {\n $ownerId = null;\n $profile = null;\n if (! empty($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->getCachedOwnerProfile((string) $ownerId);\n }\n\n $name = 'Unknown';\n if (isset($properties['dealname'])) {\n $name = mb_strimwidth($properties['dealname'], 0, 128);\n }\n\n $amount = $this->resolveAmount($properties);\n $currency = $properties['deal_currency_code'] ?? null;\n\n $closeDate = null;\n if (! empty($properties['closedate'])) {\n $closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');\n }\n\n $remotelyCreatedAt = null;\n if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {\n $date = $this->parseCleanDatetime($properties['createdate']);\n $remotelyCreatedAt = $date?->format('Y-m-d H:i:s');\n }\n\n $closedStages = $this->getClosedDealStages();\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $data = [\n 'team_id' => $this->team->getId(),\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'value' => ! empty($amount) ? $amount : null,\n 'currency_code' => CurrencyFormatter::formatCode($currency),\n 'close_date' => $closeDate,\n 'is_closed' => $isWon || $isLost,\n 'is_won' => $isWon,\n 'remotely_created_at' => $remotelyCreatedAt,\n 'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),\n 'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),\n ];\n\n if ($accountId) {\n $data['account_id'] = $accountId;\n }\n\n if ($stage) {\n $data['stage_id'] = $stage->id;\n }\n\n if ($businessProcess) {\n $recordType = $this->getCachedBusinessProcessRecordType($businessProcess);\n if ($recordType) {\n $data['record_type_id'] = $recordType->id;\n }\n }\n\n return $data;\n }\n\n private function getCachedOwnerProfile(string $ownerId): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $ownerId;\n if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {\n return $this->cachedOwnerProfiles[$cacheKey];\n }\n\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);\n $this->cachedOwnerProfiles[$cacheKey] = $profile;\n\n return $profile;\n }\n\n private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed\n {\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId();\n if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {\n return $this->cachedRecordTypes[$cacheKey];\n }\n\n $recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);\n $this->cachedRecordTypes[$cacheKey] = $recordType;\n\n return $recordType;\n }\n\n private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess\n {\n if ($pipelineId === null) {\n return null;\n }\n\n $cacheKey = $this->getBusinessProcessCacheKey($pipelineId);\n if (isset($this->cachedBusinessProcesses[$cacheKey])) {\n return $this->cachedBusinessProcesses[$cacheKey];\n }\n\n $businessProcess = $this->getBusinessProcess($pipelineId);\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->importStages();\n $businessProcess = $this->getBusinessProcess($pipelineId);\n }\n\n if (! $businessProcess instanceof BusinessProcess) {\n $this->logger->info(\n '[HubSpot] Deal is not attached to a pipeline',\n [\n 'pipeline' => $pipelineId]\n );\n }\n\n $this->cachedBusinessProcesses[$cacheKey] = $businessProcess;\n\n return $businessProcess;\n }\n\n private function getBusinessProcess(string $pipelineId): ?BusinessProcess\n {\n return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);\n }\n\n private function getBusinessProcessCacheKey(string $pipelineId): string\n {\n return $this->config->getId() . '_' . $pipelineId;\n }\n\n private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage\n {\n if (empty($stageId)) {\n return null;\n }\n\n $cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;\n if (isset($this->cachedStages[$cacheKey])) {\n return $this->cachedStages[$cacheKey];\n }\n\n $stage = $this->crmEntityRepository->getPipelineStageByConditions(\n $businessProcess,\n [\n 'crm_provider_id' => $stageId,\n 'type' => Stage::TYPE_OPPORTUNITY,\n ]\n );\n\n if ($stage === null) {\n $this->importStages(null, $stageId);\n }\n\n if ($stage === null) {\n $this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);\n }\n\n $this->cachedStages[$cacheKey] = $stage;\n\n return $stage;\n }\n\n private function resolveAmount(array $properties): ?string\n {\n $amount = null;\n if (! empty($properties['amount'])) {\n $amount = str_replace(',', '', $properties['amount']);\n }\n\n if ($this->config->hasDefaultCurrencyFieldSet()) {\n $valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();\n $amount = $properties[$valueFieldName] ?? $amount;\n }\n\n return $amount;\n }\n\n private function parseCleanDatetime(string $datetime): ?Carbon\n {\n // Treat pre-1980 values as invalid\n $minValidDate = Carbon::parse('1980-01-01 00:00:00');\n\n try {\n $date = Carbon::parse($datetime);\n\n if ($minValidDate->gt($date)) {\n return null;\n }\n\n return $date;\n } catch (Exception) {\n return null; // On parse error, treat as null\n }\n }\n\n private function resolveDealProbability(?string $stageProbability): int\n {\n if ($stageProbability === null) {\n return 0;\n }\n\n $probability = (float) $stageProbability;\n\n return $probability > 1 ? 0 : (int) ($probability * 100);\n }\n\n private function resolveForecastCategory(?string $forecastCategory): string\n {\n if (! $forecastCategory) {\n return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;\n }\n\n $forecastCategory = str_replace('_', ' ', $forecastCategory);\n\n return ucwords(strtolower($forecastCategory));\n }\n\n private function importExternalFieldData(array $properties, int $opportunityId): void\n {\n $this->importOpportunityCrmFieldData(\n $properties,\n $this->getCachedOpportunitySyncableFields(),\n $opportunityId\n );\n }\n\n private function getCachedOpportunitySyncableFields(): array\n {\n $cacheKey = (string) $this->config->getId();\n if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {\n $this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();\n }\n\n return $this->cachedOpportunitySyncableFields[$cacheKey];\n }\n\n private function importOpportunityContacts(Opportunity $opportunity, array $associations): void\n {\n // Handle empty or missing contact associations\n if (empty($associations)) {\n // Remove all existing contact associations if none provided\n $this->removeAllOpportunityContacts($opportunity);\n\n return;\n }\n\n // Use differential sync approach for better performance and accuracy\n $this->syncOpportunityContactsDifferential($opportunity, $associations);\n }\n\n /**\n * Sync opportunity contacts using differential approach\n * This compares current vs new associations and only makes necessary changes\n */\n private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void\n {\n $currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);\n $contactAssociationIds = array_keys($contactAssociations);\n\n $contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);\n $contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);\n\n if (empty($contactsToAdd) && empty($contactsToRemove)) {\n return;\n }\n\n $this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);\n\n $this->removeContactAssociations($opportunity, $contactsToRemove);\n $this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);\n }\n\n private function getCurrentContactCrmIds(Opportunity $opportunity): array\n {\n return $opportunity->contacts()\n ->pluck('contacts.crm_provider_id')\n ->toArray();\n }\n\n private function logContactAssociationChanges(\n Opportunity $opportunity,\n array $currentContactCrmIds,\n array $contactAssociations,\n array $contactsToAdd,\n array $contactsToRemove\n ): void {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [\n 'opportunity_id' => $opportunity->getId(),\n 'current_contacts' => $currentContactCrmIds,\n 'new_contacts' => $contactAssociations,\n 'contacts_to_add' => $contactsToAdd,\n 'contacts_to_remove' => $contactsToRemove,\n ]);\n }\n\n private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void\n {\n if (empty($contactsToRemove)) {\n return;\n }\n\n $contactsToDetach = $opportunity->contacts()\n ->whereIn('contacts.crm_provider_id', $contactsToRemove)\n ->pluck('contacts.id')\n ->toArray();\n\n if (! empty($contactsToDetach)) {\n $opportunity->contacts()->detach($contactsToDetach);\n\n $this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'removed_contact_crm_ids' => $contactsToRemove,\n 'removed_contact_count' => count($contactsToDetach),\n ]);\n }\n }\n\n private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void\n {\n if (empty($contactsToAdd)) {\n return;\n }\n\n $contactsAdded = [];\n foreach ($contactsToAdd as $crmId) {\n $id = $contactAssociations[$crmId];\n\n if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {\n $contactsAdded[] = $crmId;\n }\n }\n\n $this->logAddedContacts($opportunity, $contactsAdded);\n }\n\n private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool\n {\n try {\n return $this->performContactAttachment($opportunity, $id, $crmId);\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [\n 'opportunity_id' => $opportunity->getId(),\n 'contact_crm_id' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool\n {\n try {\n $opportunity->contacts()->attach($contactId, [\n 'crm_provider_id' => $crmId,\n ]);\n\n return true;\n } catch (\\Illuminate\\Database\\QueryException $e) {\n if (str_contains($e->getMessage(), 'Duplicate entry')) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [\n 'contact_id' => $contactId,\n 'contact_crm_id' => $crmId,\n 'opportunity_id' => $opportunity->getId(),\n ]);\n\n return false;\n }\n\n throw $e;\n }\n }\n\n private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void\n {\n if (! empty($contactsAdded)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [\n 'opportunity_id' => $opportunity->getId(),\n 'added_contact_crm_ids' => $contactsAdded,\n 'added_contacts_count' => count($contactsAdded),\n ]);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5892890200228759586
|
-8493339382995643936
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
32
2
22
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use HubSpot\Client\Crm\Deals\Model\CollectionResponseAssociatedId;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Models\Account;
use Exception;
use Jiminny\Component\DealInsights\Forecast\Forecast;
use Jiminny\Jobs\Crm\MatchActivitiesToNewOpportunity;
use Jiminny\Models\Contact;
use Jiminny\Models\Crm\BusinessProcess;
use Jiminny\Exceptions\CrmException;
use Jiminny\Models\Opportunity;
use Illuminate\Support\Collection;
use Jiminny\Models\Stage;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Crm\Hubspot\DealFieldsService;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\HubspotSingleSyncStrategy;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Services\Crm\OpportunitySyncStrategyResolver;
use Jiminny\Utils\CurrencyFormatter;
/**
* Optimized sync methods for better performance
* These methods can be integrated into SyncCrmEntitiesTrait for significant performance gains
*/
trait OpportunitySyncTrait
{
private const int BATCH_SIZE = 100;
private const int BATCH_PROCESS_SIZE = 800;
protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;
protected CrmEntityRepository $crmEntityRepository;
protected DealFieldsService $dealFieldsService;
private ?array $cachedClosedDealStages = null;
private array $cachedBusinessProcesses = [];
private array $cachedStages = [];
/** @var array<string, array<string>> keyed by config id */
private array $cachedOpportunitySyncableFields = [];
/** @var array<string, mixed> keyed by configId:ownerId */
private array $cachedOwnerProfiles = [];
/** @var array<string, mixed> keyed by configId:businessProcessId */
private array $cachedRecordTypes = [];
public function syncOpportunities(array $parameters, ?string $strategy = null): int
{
$startTime = microtime(true);
$strategies = $this->opportunitySyncStrategyResolver->getStrategies($this->config, $strategy);
$parameters['config'] = $this->config;
$syncCount = 0;
$reportedTotal = 0;
$lastSyncedId = [];
$strategyNames = [];
try {
foreach ($strategies as $strategyName => $syncStrategy) {
$strategyNames[] = $strategyName;
$this->logger->info(
'[' . $this->getDisplayName() . '] Syncing opportunities using strategy: ' . $strategyName,
['team' => $this->team->getId()]
);
$total = 0;
$lastId = null;
$buffer = [];
// HubspotWebhookBatchSyncStrategy returns empty generator, this is for other strategies
foreach ($syncStrategy->fetchOpportunities($parameters, $total, $lastId) as $hsOpportunity) {
$buffer[] = $hsOpportunity;
// process every 800 rows (fits < 1 000 association limit)
if (\count($buffer) >= self::BATCH_PROCESS_SIZE) {
$syncCount += $this->processOpportunityBatch($buffer);
$buffer = [];
}
}
// leftovers
if ($buffer) {
$syncCount += $this->processOpportunityBatch($buffer);
}
$reportedTotal += $total;
$lastSyncedId = $lastId;
}
} catch (\HubSpot\Client\Crm\Deals\ApiException | CrmException $e) {
$this->handleSyncException($e, $parameters);
}
$durationMs = round((microtime(true) - $startTime) * 1000, 2);
$this->logger->info(
'[HubSpot] Synced opportunities',
[
'team' => $this->team->getId(),
'strategies' => implode(',', $strategyNames),
'sync_count' => $syncCount,
'total' => $reportedTotal,
'last_synced_id' => $lastSyncedId,
'duration_ms' => $durationMs,
]
);
return $reportedTotal;
}
private function handleSyncException(\Throwable $e, array $parameters): void
{
if (($parameters['since'] ?? null) instanceof Carbon) {
$parameters['since'] = $parameters['since']->toDateTimeString();
}
$parameters['config'] = $this->config->getId();
$this->logger->warning('[' . $this->getDisplayName() . '] Sync opportunities failed', [
'teamId' => $this->team->getUuid(),
'parameters' => $parameters,
'reason' => $e->getMessage(),
]);
}
/**
* @inheritdoc
*/
public function syncOpportunity(string $crmId): ?Opportunity
{
$strategy = $this->opportunitySyncStrategyResolver->resolve(
$this->config,
OpportunitySyncStrategyResolver::SINGLE_SYNC_OPPORTUNITY_STRATEGY,
);
$parameters = [
'config' => $this->config,
'crm_id' => $crmId,
];
try {
if (! $strategy instanceof HubspotSingleSyncStrategy) {
throw new InvalidArgumentException('Strategy must by HubspotSingleSyncStrategy');
}
$hsOpportunity = $strategy->fetchOpportunity($parameters);
} catch (\HubSpot\Client\Crm\Deals\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Opportunity not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
$hsOpportunity['associations'] = $this->convertDealAssociations($hsOpportunity['associations'] ?? []);
return $this->importOrUpdateOpportunity($hsOpportunity);
}
/**
* Process webhook-collected opportunity batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportOpportunityBatch jobs for batch processing.
*
* @return int Number of opportunity IDs dispatched to jobs
*/
public function batchSyncOpportunities(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
}
/**
* Import a batch of opportunities by their CRM IDs.
* Fetches opportunity data from HubSpot API and delegates to importOpportunityBatch().
*
* @param array<string> $crmIds HubSpot deal CRM IDs
*
* @return array{success: array, failed_ids: array, errors?: array<string, string>}
*/
public function importOpportunityBatchByIds(array $crmIds): array
{
$fields = $this->dealFieldsService->getFieldsForConfiguration($this->config);
$allDeals = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
$deals = $this->client->getOpportunitiesByIds($chunk, $fields);
foreach ($deals as $deal) {
$allDeals[] = $deal;
}
}
// IDs not returned by HubSpot are likely deleted or inaccessible deals.
// These are not failures — retrying won't bring them back.
$fetchedIds = array_map('strval', array_column($allDeals, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] CRM IDs not found in HubSpot (likely deleted)', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allDeals),
]);
}
if (empty($allDeals)) {
return ['success' => [], 'failed_ids' => []];
}
return $this->importOpportunityBatch($allDeals);
}
private function getClosedDealStages(): array
{
if ($this->cachedClosedDealStages !== null) {
return $this->cachedClosedDealStages;
}
$stages = $this->crmEntityRepository->getOpportunityClosedStages($this->config);
$data = [
'lost' => [],
'won' => [],
];
foreach ($stages as $stage) {
if ($stage->probability == 0.00) {
$data['lost'][] = $stage->crm_provider_id;
}
if ($stage->probability == 100.00) {
$data['won'][] = $stage->crm_provider_id;
}
}
$this->cachedClosedDealStages = $data;
return $data;
}
/**
* Import deals into the database with pre-fetched associations.
*
* API calls here (getAssociationsData, getExistingOpportunityCrmIds) are NOT
* caught — if they throw, the exception propagates to ImportOpportunityBatch::handle()
* where Laravel retries the whole job with backoff. After all retries exhausted,
* failed() requeues all IDs to Redis.
*
* The per-deal loop catches exceptions individually. A deal can end up in three states:
* - success: imported/updated successfully
* - failed_ids: exception thrown (DB constraint violation, corrupt data, etc.)
* These are permanent issues — retrying won't fix them.
* - skipped (null): missing dependencies (no account, unknown pipeline/stage).
* This is acceptable — the deal cannot be imported until those exist.
*/
private function importOpportunityBatch(array $deals): array
{
$syncedOpportunities = [
'success' => [],
'failed_ids' => [],
];
$dealIds = array_column($deals, 'id');
$batchStart = microtime(true);
$slowDeals = [];
// Shared association/existing-ID preparation is batch-level state. If it fails, rethrow so the
// queue job retries the whole batch and eventually requeues all deal IDs back to Redis.
try {
$companyAssocStart = microtime(true);
$companyAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'companies');
$companyAssocMs = (int) round((microtime(true) - $companyAssocStart) * 1000);
$contactAssocStart = microtime(true);
$contactAssociations = $this->client->getAssociationsData($dealIds, 'deals', 'contacts');
$contactAssocMs = (int) round((microtime(true) - $contactAssocStart) * 1000);
$prepareStart = microtime(true);
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
$prepareTimings = [];
$associationsData = $this->prepareAssociatedEntities(
$companyAssociations,
$contactAssociations,
$prepareTimings
);
$prepareMs = (int) round((microtime(true) - $prepareStart) * 1000);
$missingCompanies = count(array_diff(
$allCompanyIds,
array_keys($associationsData['company_id_mappings'] ?? [])
));
$missingContacts = count(array_diff(
$allContactIds,
array_keys($associationsData['contact_id_mappings'] ?? [])
));
$existingCrmIds = $this->crmEntityRepository->getExistingOpportunityCrmIds(
$this->config,
array_map('strval', $dealIds)
);
$existingCrmIdSet = array_flip($existingCrmIds);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to fetch associations or existing IDs', [
'teamId' => $this->team->getId(),
'dealCount' => count($dealIds),
'error' => $e->getMessage(),
]);
throw $e;
}
$loopStart = microtime(true);
foreach ($deals as $deal) {
$dealStart = microtime(true);
try {
$deal['associations'] = $this->prepareAssociationsForOpportunity(
$deal['id'],
$companyAssociations,
$contactAssociations,
$associationsData
);
$syncedOpportunity = $this->importOrUpdateOpportunity(
$deal,
isset($existingCrmIdSet[(string) $deal['id']])
);
if ($syncedOpportunity) {
$syncedOpportunities['success'][] = $syncedOpportunity;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import opportunity', [
'teamId' => $this->team->getId(),
'crmId' => $deal['id'],
'error' => $e->getMessage(),
]);
$syncedOpportunities['failed_ids'][] = $deal['id'];
$syncedOpportunities['errors'][$deal['id']] = $e->getMessage();
}
$dealMs = (int) round((microtime(true) - $dealStart) * 1000);
if ($dealMs > 1000) {
$slowDeals[] = ['crmId' => $deal['id'], 'ms' => $dealMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importOpportunityBatch timing', [
'teamId' => $this->team->getId(),
'deal_count' => count($deals),
'total_ms' => $totalMs,
'company_assoc_api_ms' => $companyAssocMs,
'contact_assoc_api_ms' => $contactAssocMs,
'prepare_entities_ms' => $prepareMs,
'prepare_accounts_ms' => $prepareTimings['accounts_ms'],
'prepare_contacts_ms' => $prepareTimings['contacts_ms'],
'total_companies' => count($allCompanyIds),
'missing_companies' => $missingCompanies,
'total_contacts' => count($allContactIds),
'missing_contacts' => $missingContacts,
'deals_loop_ms' => $loopMs,
'avg_deal_ms' => ! empty($deals) ? (int) round($loopMs / count($deals)) : 0,
'slow_deals_count' => count($slowDeals),
'slow_deals' => array_slice($slowDeals, 0, 10),
]);
return $syncedOpportunities;
}
/**
* Prepare associated entities for opportunities with optimized batch processing
* Returns structured data with CRM ID to DB ID mappings for each opportunity
*/
private function prepareAssociatedEntities(
array $companyAssociations,
array $contactAssociations,
array &$timings = []
): array {
// Step 1: Collect all unique company and contact IDs from associations
$allCompanyIds = $this->flattenAssociationIds($companyAssociations);
$allContactIds = $this->flattenAssociationIds($contactAssociations);
// Step 2: Batch sync missing entities and get CRM ID to DB ID mappings
$companyIdMappings = [];
$contactIdMappings = [];
$accountsMs = 0;
$contactsMs = 0;
if (! empty($allCompanyIds)) {
$start = microtime(true);
$companyIdMappings = $this->prepareAssociatedAccounts($allCompanyIds);
$accountsMs = (int) round((microtime(true) - $start) * 1000);
}
if (! empty($allContactIds)) {
$start = microtime(true);
$contactIdMappings = $this->prepareAssociatedContacts($allContactIds);
$contactsMs = (int) round((microtime(true) - $start) * 1000);
}
$timings = [
'accounts_ms' => $accountsMs,
'contacts_ms' => $contactsMs,
];
return [
'company_id_mappings' => $companyIdMappings,
'contact_id_mappings' => $contactIdMappings,
];
}
/**
* Flatten association data to get unique IDs
*/
private function flattenAssociationIds(array $associations): array
{
$ids = [];
foreach ($associations as $dealAssociations) {
if (is_array($dealAssociations)) {
foreach ($dealAssociations as $id) {
$ids[$id] = true;
}
}
}
return array_keys($ids);
}
/**
* Batch sync missing accounts
*/
private function prepareAssociatedAccounts(array $companyIds): array
{
// Find which accounts already exist (lean covering-index lookup)
$existingAccountsData = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($existingAccountsData));
if (empty($missingCompanyIds)) {
return $existingAccountsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts', [
'teamId' => $this->team->getUuid(),
'total_companies' => count($companyIds),
'existing_companies' => count($existingAccountsData),
'missing_companies' => count($missingCompanyIds),
]);
// we already have limit on opportunity ids count
// Initialize variable before try block
$syncedAccountsData = [];
try {
$syncedAccountsData = $this->batchSyncCrmObjects('companies', $missingCompanyIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing accounts', [
'size' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
$syncedAccountsData = [];
}
return $existingAccountsData + $syncedAccountsData;
}
/**
* Prepare associated contacts - find existing and sync missing ones
* Returns mapping of CRM ID to DB ID
*/
private function prepareAssociatedContacts(array $contactIds): array
{
// Find which contacts already exist (lean covering-index lookup)
$existingContactsData = $this->crmEntityRepository
->getExistingContactIdsMap($this->config, $contactIds);
$missingContactIds = array_diff($contactIds, array_keys($existingContactsData));
if (empty($missingContactIds)) {
return $existingContactsData;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing contacts', [
'teamId' => $this->team->getUuid(),
'total_contacts' => count($contactIds),
'existing_contacts' => count($existingContactsData),
'missing_contacts' => count($missingContactIds),
]);
// Sync missing contacts using batch API
try {
$syncedContactsData = $this->batchSyncCrmObjects('contacts', $missingContactIds);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to sync missing contacts', [
'size' => count($missingContactIds),
'error' => $e->getMessage(),
]);
$syncedContactsData = [];
}
return $existingContactsData + $syncedContactsData;
}
private function batchSyncCrmObjects(string $objectType, array $crmIds): array
{
$syncObjects = [];
$crmObjectIds = array_values($crmIds);
foreach (array_chunk($crmObjectIds, self::BATCH_SIZE) as $chunk) {
try {
$objects = $objectType === 'companies' ?
$this->client->getCompaniesByIds($chunk, $this->getCompanyFields()) :
$this->client->getContactsByIds($chunk, $this->getContactFields());
foreach ($objects as $objectId => $objectData) {
$this->importCrmObject($objectType, (string) $objectId, $objectData, $syncObjects);
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch synced ' . $objectType, [
'requested_count' => count($chunk),
'synced_count' => count($objects),
]);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch ' . $objectType . ' sync failed', [
'ids' => $chunk,
'error' => $e->getMessage(),
]);
}
}
return $syncObjects;
}
private function importCrmObject(string $objectType, string $objectId, mixed $objectData, array &$syncObjects): void
{
try {
$object = $objectType === 'companies' ?
$this->importAccount($objectData) :
$this->importContact($objectData);
if ($object) {
$syncObjects[$object->getCrmProviderId()] = $object->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import batch ' . $objectType, [
'id' => $objectId,
'error' => $e->getMessage(),
]);
}
}
/**
* Prepare associations for a single opportunity
*
* The return value is an array with the following structure:
* [
* 'companies' => [
* $companyCrmId => $companyId,
* ...
* ],
* 'contacts' => [
* $contactCrmId => $contactId,
* ...
* ],
* 'account_id' => $accountId,
* ]
*/
private function prepareAssociationsForOpportunity(
string $oppCrmId,
array $companyAssociations,
array $contactAssociations,
array $associationsData
): array {
$associations = [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
$oppCompanyIds = $companyAssociations[$oppCrmId] ?? [];
foreach ($oppCompanyIds as $companyCrmId) {
if (isset($associationsData['company_id_mappings'][$companyCrmId])) {
$associations['companies'][$companyCrmId] = $associationsData['company_id_mappings'][$companyCrmId];
// Set primary account (first company becomes primary account)
if ($associations['account_id'] === null) {
$associations['account_id'] = $associationsData['company_id_mappings'][$companyCrmId];
}
}
}
$oppContactIds = $contactAssociations[$oppCrmId] ?? [];
foreach ($oppContactIds as $contactCrmId) {
if (isset($associationsData['contact_id_mappings'][$contactCrmId])) {
$associations['contacts'][$contactCrmId] = $associationsData['contact_id_mappings'][$contactCrmId];
}
}
return $associations;
}
/**
* Update only associations for an opportunity
*/
private function updateOpportunityAssociations(Opportunity $opportunity, array $associations): void
{
// Update contact associations
$this->importOpportunityContacts($opportunity, $associations['contacts']);
// Update company (account) associations
$this->updateOpportunityAccount($opportunity, $associations['account_id']);
}
/**
* Remove all contact associations from an opportunity
*/
private function removeAllOpportunityContacts(Opportunity $opportunity): void
{
$currentCount = (int) $opportunity->contacts()->count();
if ($currentCount > 0) {
$opportunity->contacts()->detach();
$this->logger->info('[' . $this->getDisplayName() . '] Removed all contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_count' => $currentCount,
]);
}
}
private function updateOpportunityAccount(Opportunity $opportunity, ?int $accountId): void
{
if ($accountId === null) {
// No account ID provided - keep current account
return;
}
$currentAccountId = $opportunity->getAccountId();
// Only update if account has changed
if ($currentAccountId !== $accountId) {
$opportunity->account_id = $accountId;
$opportunity->save();
$this->logger->info('[' . $this->getDisplayName() . '] Updated opportunity account association', [
'opportunity_id' => $opportunity->getId(),
'old_account_id' => $currentAccountId,
'new_account_id' => $accountId,
]);
}
}
/**
* Find existing opportunities by external IDs (OPTIMIZED VERSION)
* Uses batch query for better performance
*/
private function findExistingOpportunities(array $crmIds): Collection
{
return $this->crmEntityRepository
->findOpportunitiesByExternalIds($this->config, $crmIds);
}
private function processOpportunityBatch(array $opportunities): int
{
$syncedOpportunities = $this->importOpportunityBatch($opportunities);
return count($syncedOpportunities['success'] ?? []);
}
/**
* Convert single deal associations from HubSpot format to internal format
* Handles both HubSpot SDK objects and array formats
*
* @param array $opportunityAssociations Raw associations from HubSpot API or pre-processed
*
* @return array Processed associations with DB IDs
*/
private function convertDealAssociations(array $opportunityAssociations): array
{
$associations = $this->initializeAssociationsStructure();
if (empty($opportunityAssociations)) {
return $associations;
}
$associationIds = $this->extractAssociationIds($opportunityAssociations);
$this->processCompanyAssociations($associationIds, $associations);
$this->processContactAssociations($associationIds, $associations);
return $associations;
}
private function initializeAssociationsStructure(): array
{
return [
'companies' => [],
'contacts' => [],
'account_id' => null, // Primary account for opportunity
];
}
private function extractAssociationIds(array $opportunityAssociations): array
{
$associationIds = [];
foreach ($opportunityAssociations as $type => $associationData) {
if (! empty($associationData)) {
$associationIds[$type] = $this->convertSingleDealAssociations($associationData);
}
}
return $associationIds;
}
private function processCompanyAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['companies'])) {
return;
}
$companyId = $associationIds['companies'][0];
$account = $this->findOrSyncAccount($companyId);
if ($account instanceof Account) {
$associations['companies'][$companyId] = $account->getId();
$associations['account_id'] = $account->getId();
}
}
private function processContactAssociations(array $associationIds, array &$associations): void
{
if (empty($associationIds['contacts'])) {
return;
}
foreach ($associationIds['contacts'] as $contactId) {
$contact = $this->findOrSyncContact($contactId);
if ($contact instanceof Contact) {
$associations['contacts'][$contactId] = $contact->getId();
}
}
}
private function findOrSyncAccount(string $companyId): ?Account
{
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $companyId);
if (! $account instanceof Account) {
$account = $this->syncAccount($companyId);
}
return $account;
}
private function findOrSyncContact(string $contactId): ?Contact
{
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $contactId);
if (! $contact instanceof Contact) {
$contact = $this->syncContact($contactId);
}
return $contact;
}
private function convertSingleDealAssociations($opportunityAssociations = null): array
{
$associationData = [];
if ($opportunityAssociations === null) {
return $associationData;
}
// Handle array input (from extractAssociationIds)
if (is_array($opportunityAssociations)) {
return $opportunityAssociations;
}
// Handle CollectionResponseAssociatedId object
if ($opportunityAssociations instanceof CollectionResponseAssociatedId) {
foreach ($opportunityAssociations->getResults() as $association) {
$associationData[] = $association->getId();
}
}
return $associationData;
}
private function importOrUpdateOpportunity($crmData, ?bool $exists = null): ?Opportunity
{
if (empty($crmData['properties'])) {
return null;
}
$crmId = (string) $crmData['id'];
$properties = $crmData['properties'];
$associations = $crmData['associations'] ?? [];
$opportunityExists = $exists ?? (bool) $this->crmEntityRepository->findOpportunityByExternalId(
$this->config,
$crmId
);
if ($opportunityExists) {
return $this->updateOpportunity($crmId, $properties, $associations);
}
return $this->createOpportunity($crmId, $properties, $associations);
}
/**
* Create new opportunity
*/
private function createOpportunity(string $crmId, array $properties, array $associations): ?Opportunity
{
$accountId = $this->resolveAccountId($associations);
if (! $accountId) {
return null;
}
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
if (! $businessProcess) {
return null;
}
$stage = $this->resolveStage($businessProcess, $properties['dealstage'] ?? null);
if (! $stage) {
return null;
}
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->importOpportunityContacts($opportunity, $associations['contacts']);
if ($opportunity->wasRecentlyCreated) {
MatchActivitiesToNewOpportunity::dispatch($opportunity->getId());
}
return $opportunity;
}
/**
* Update existing opportunity
*/
private function updateOpportunity(string $crmId, array $properties, array $associations): Opportunity
{
$accountId = $this->resolveAccountId($associations);
$businessProcess = $this->resolveBusinessProcess($properties['pipeline'] ?? null);
$stage = $businessProcess ? $this->resolveStage($businessProcess, $properties['dealstage'] ?? null) : null;
$data = $this->buildOpportunityData($properties, $accountId, $businessProcess, $stage);
$attributes = [
'crm_configuration_id' => $this->config->getId(),
'crm_provider_id' => $crmId,
];
$values = array_merge($attributes, $data);
$opportunity = $this->crmEntityRepository->upsertOpportunity($attributes, $values);
$this->importExternalFieldData($properties, $opportunity->getId());
$this->updateOpportunityAssociations($opportunity, $associations);
return $opportunity;
}
private function resolveAccountId(array $associations): ?int
{
if (! empty($associations['account_id'])) {
return $associations['account_id'];
}
if (empty($associations)) {
return null;
}
// Fallback: use first company as account (currently SDK returns one company)
foreach ($associations['companies'] as $accountId) {
return $accountId;
}
return null;
}
private function buildOpportunityData(
array $properties,
?int $accountId,
?BusinessProcess $businessProcess,
?Stage $stage
): array {
$ownerId = null;
$profile = null;
if (! empty($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->getCachedOwnerProfile((string) $ownerId);
}
$name = 'Unknown';
if (isset($properties['dealname'])) {
$name = mb_strimwidth($properties['dealname'], 0, 128);
}
$amount = $this->resolveAmount($properties);
$currency = $properties['deal_currency_code'] ?? null;
$closeDate = null;
if (! empty($properties['closedate'])) {
$closeDate = Carbon::parse($properties['closedate'])->format('Y-m-d');
}
$remotelyCreatedAt = null;
if (! empty($properties['createdate']) && strtotime($properties['createdate'])) {
$date = $this->parseCleanDatetime($properties['createdate']);
$remotelyCreatedAt = $date?->format('Y-m-d H:i:s');
}
$closedStages = $this->getClosedDealStages();
$isWon = in_array($properties['dealstage'], $closedStages['won']);
$isLost = in_array($properties['dealstage'], $closedStages['lost']);
$data = [
'team_id' => $this->team->getId(),
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => $name,
'value' => ! empty($amount) ? $amount : null,
'currency_code' => CurrencyFormatter::formatCode($currency),
'close_date' => $closeDate,
'is_closed' => $isWon || $isLost,
'is_won' => $isWon,
'remotely_created_at' => $remotelyCreatedAt,
'probability' => $this->resolveDealProbability($properties['hs_deal_stage_probability']),
'forecast_category' => $this->resolveForecastCategory($properties['hs_manual_forecast_category']),
];
if ($accountId) {
$data['account_id'] = $accountId;
}
if ($stage) {
$data['stage_id'] = $stage->id;
}
if ($businessProcess) {
$recordType = $this->getCachedBusinessProcessRecordType($businessProcess);
if ($recordType) {
$data['record_type_id'] = $recordType->id;
}
}
return $data;
}
private function getCachedOwnerProfile(string $ownerId): mixed
{
$cacheKey = $this->config->getId() . ':' . $ownerId;
if (array_key_exists($cacheKey, $this->cachedOwnerProfiles)) {
return $this->cachedOwnerProfiles[$cacheKey];
}
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, $ownerId);
$this->cachedOwnerProfiles[$cacheKey] = $profile;
return $profile;
}
private function getCachedBusinessProcessRecordType(BusinessProcess $businessProcess): mixed
{
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId();
if (array_key_exists($cacheKey, $this->cachedRecordTypes)) {
return $this->cachedRecordTypes[$cacheKey];
}
$recordType = $this->crmEntityRepository->getBusinessProcessRecordType($businessProcess);
$this->cachedRecordTypes[$cacheKey] = $recordType;
return $recordType;
}
private function resolveBusinessProcess(?string $pipelineId): ?BusinessProcess
{
if ($pipelineId === null) {
return null;
}
$cacheKey = $this->getBusinessProcessCacheKey($pipelineId);
if (isset($this->cachedBusinessProcesses[$cacheKey])) {
return $this->cachedBusinessProcesses[$cacheKey];
}
$businessProcess = $this->getBusinessProcess($pipelineId);
if (! $businessProcess instanceof BusinessProcess) {
$this->importStages();
$businessProcess = $this->getBusinessProcess($pipelineId);
}
if (! $businessProcess instanceof BusinessProcess) {
$this->logger->info(
'[HubSpot] Deal is not attached to a pipeline',
[
'pipeline' => $pipelineId]
);
}
$this->cachedBusinessProcesses[$cacheKey] = $businessProcess;
return $businessProcess;
}
private function getBusinessProcess(string $pipelineId): ?BusinessProcess
{
return $this->crmEntityRepository->findBusinessProcessesByExternalId($this->config, $pipelineId);
}
private function getBusinessProcessCacheKey(string $pipelineId): string
{
return $this->config->getId() . '_' . $pipelineId;
}
private function resolveStage(BusinessProcess $businessProcess, ?string $stageId): ?Stage
{
if (empty($stageId)) {
return null;
}
$cacheKey = $this->config->getId() . ':' . $businessProcess->getId() . ':' . $stageId;
if (isset($this->cachedStages[$cacheKey])) {
return $this->cachedStages[$cacheKey];
}
$stage = $this->crmEntityRepository->getPipelineStageByConditions(
$businessProcess,
[
'crm_provider_id' => $stageId,
'type' => Stage::TYPE_OPPORTUNITY,
]
);
if ($stage === null) {
$this->importStages(null, $stageId);
}
if ($stage === null) {
$this->logger->info('[HubSpot] Stage does not exist => ' . $stageId);
}
$this->cachedStages[$cacheKey] = $stage;
return $stage;
}
private function resolveAmount(array $properties): ?string
{
$amount = null;
if (! empty($properties['amount'])) {
$amount = str_replace(',', '', $properties['amount']);
}
if ($this->config->hasDefaultCurrencyFieldSet()) {
$valueFieldName = $this->config->getDefaultCurrencyField()->getCrmProviderId();
$amount = $properties[$valueFieldName] ?? $amount;
}
return $amount;
}
private function parseCleanDatetime(string $datetime): ?Carbon
{
// Treat pre-1980 values as invalid
$minValidDate = Carbon::parse('1980-01-01 00:00:00');
try {
$date = Carbon::parse($datetime);
if ($minValidDate->gt($date)) {
return null;
}
return $date;
} catch (Exception) {
return null; // On parse error, treat as null
}
}
private function resolveDealProbability(?string $stageProbability): int
{
if ($stageProbability === null) {
return 0;
}
$probability = (float) $stageProbability;
return $probability > 1 ? 0 : (int) ($probability * 100);
}
private function resolveForecastCategory(?string $forecastCategory): string
{
if (! $forecastCategory) {
return Forecast::FORECAST_CATEGORY_UNCATEGORIZED;
}
$forecastCategory = str_replace('_', ' ', $forecastCategory);
return ucwords(strtolower($forecastCategory));
}
private function importExternalFieldData(array $properties, int $opportunityId): void
{
$this->importOpportunityCrmFieldData(
$properties,
$this->getCachedOpportunitySyncableFields(),
$opportunityId
);
}
private function getCachedOpportunitySyncableFields(): array
{
$cacheKey = (string) $this->config->getId();
if (! isset($this->cachedOpportunitySyncableFields[$cacheKey])) {
$this->cachedOpportunitySyncableFields[$cacheKey] = $this->getOpportunitySyncableFields();
}
return $this->cachedOpportunitySyncableFields[$cacheKey];
}
private function importOpportunityContacts(Opportunity $opportunity, array $associations): void
{
// Handle empty or missing contact associations
if (empty($associations)) {
// Remove all existing contact associations if none provided
$this->removeAllOpportunityContacts($opportunity);
return;
}
// Use differential sync approach for better performance and accuracy
$this->syncOpportunityContactsDifferential($opportunity, $associations);
}
/**
* Sync opportunity contacts using differential approach
* This compares current vs new associations and only makes necessary changes
*/
private function syncOpportunityContactsDifferential(Opportunity $opportunity, array $contactAssociations): void
{
$currentContactCrmIds = $this->getCurrentContactCrmIds($opportunity);
$contactAssociationIds = array_keys($contactAssociations);
$contactsToAdd = array_diff($contactAssociationIds, $currentContactCrmIds);
$contactsToRemove = array_diff($currentContactCrmIds, $contactAssociationIds);
if (empty($contactsToAdd) && empty($contactsToRemove)) {
return;
}
$this->logContactAssociationChanges($opportunity, $currentContactCrmIds, $contactAssociations, $contactsToAdd, $contactsToRemove);
$this->removeContactAssociations($opportunity, $contactsToRemove);
$this->addContactAssociations($opportunity, $contactsToAdd, $contactAssociations);
}
private function getCurrentContactCrmIds(Opportunity $opportunity): array
{
return $opportunity->contacts()
->pluck('contacts.crm_provider_id')
->toArray();
}
private function logContactAssociationChanges(
Opportunity $opportunity,
array $currentContactCrmIds,
array $contactAssociations,
array $contactsToAdd,
array $contactsToRemove
): void {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association changes', [
'opportunity_id' => $opportunity->getId(),
'current_contacts' => $currentContactCrmIds,
'new_contacts' => $contactAssociations,
'contacts_to_add' => $contactsToAdd,
'contacts_to_remove' => $contactsToRemove,
]);
}
private function removeContactAssociations(Opportunity $opportunity, array $contactsToRemove): void
{
if (empty($contactsToRemove)) {
return;
}
$contactsToDetach = $opportunity->contacts()
->whereIn('contacts.crm_provider_id', $contactsToRemove)
->pluck('contacts.id')
->toArray();
if (! empty($contactsToDetach)) {
$opportunity->contacts()->detach($contactsToDetach);
$this->logger->info('[' . $this->getDisplayName() . '] Removed contact associations', [
'opportunity_id' => $opportunity->getId(),
'removed_contact_crm_ids' => $contactsToRemove,
'removed_contact_count' => count($contactsToDetach),
]);
}
}
private function addContactAssociations(Opportunity $opportunity, array $contactsToAdd, array $contactAssociations): void
{
if (empty($contactsToAdd)) {
return;
}
$contactsAdded = [];
foreach ($contactsToAdd as $crmId) {
$id = $contactAssociations[$crmId];
if ($this->attachSingleContact($opportunity, (string) $crmId, $id)) {
$contactsAdded[] = $crmId;
}
}
$this->logAddedContacts($opportunity, $contactsAdded);
}
private function attachSingleContact(Opportunity $opportunity, string $crmId, int $id): bool
{
try {
return $this->performContactAttachment($opportunity, $id, $crmId);
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to add contact association', [
'opportunity_id' => $opportunity->getId(),
'contact_crm_id' => $crmId,
'error' => $e->getMessage(),
]);
return false;
}
}
private function performContactAttachment(Opportunity $opportunity, int $contactId, string $crmId): bool
{
try {
$opportunity->contacts()->attach($contactId, [
'crm_provider_id' => $crmId,
]);
return true;
} catch (\Illuminate\Database\QueryException $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact association already exists', [
'contact_id' => $contactId,
'contact_crm_id' => $crmId,
'opportunity_id' => $opportunity->getId(),
]);
return false;
}
throw $e;
}
}
private function logAddedContacts(Opportunity $opportunity, array $contactsAdded): void
{
if (! empty($contactsAdded)) {
$this->logger->info('[' . $this->getDisplayName() . '] Added contact associations', [
'opportunity_id' => $opportunity->getId(),
'added_contact_crm_ids' => $contactsAdded,
'added_contacts_count' => count($contactsAdded),
]);
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2757
|
NULL
|
NULL
|
NULL
|
|
2760
|
111
|
49
|
2026-05-07T11:39:15.480587+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153955480_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotWebhookBatchSyncStrategy.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
4993317355264648582
|
-8132372645456802878
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
iTerm2• 0ShellEditViewSessionScriptsProfilesWindowHelp$0l46]Support Daily - in 21 m100% <78DEV (docker)DOCKERDEV (docker)H82APP (-zsh)-zsh• *4screenpipe"eventsroutesviewsjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: stoppedworker-download:worker-download_00:stoppedjiminny-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:stoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00: stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-emails:worker-emails_00: stoppedworker-es-update:worker-es-update_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00:startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00: startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedrootedocker_Lamp_1:/home/Jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'Syncing opportunity for HubspotSyncing opportunities modified since 2026-05-01 00:00:00...Synced 6 opportunities.root@docker_lamp_1:/home/jiminny# ]•$52.35ms DONE1.64ms DONE3.18ms DONE-zshThu 7 May 14:39:15T81₴6DEV...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2762
|
111
|
50
|
2026-05-07T11:39:17.448654+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153957448_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotWebhookBatchSyncStrategy.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\Concerns\ValidatesParameters;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
/**
* Webhook-driven batch sync strategy for HubSpot deals (opportunities).
*
* This strategy processes opportunities collected from webhook property change and
* association change events. Deal CRM IDs are accumulated in Redis sets and processed
* in batches via ImportOpportunityBatch jobs.
*
* Unlike traditional sync strategies that fetch deals from HubSpot API based on
* modification dates or other filters, this strategy only processes deals that have
* been explicitly flagged for sync via webhook events.
*
* Webhook Event Flow:
* 1. Webhook event received (deal.propertyChange or deal.associationChange)
* 2. Change Handler validates and collects deal ID
* 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all
* 4. This strategy drains Redis set during SyncObjects cycle
* 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100
*
* Benefits:
* - Near real-time sync (processes only changed deals)
* - Reduced API calls (no polling for all deals)
* - Automatic deduplication via Redis sets
* - Resilient failure handling (failed IDs requeued to Redis)
*
* Usage:
* - Automatically invoked during SyncObjects cycle for teams configured with this strategy
* - Can be manually triggered via ProcessHubspotObjectsSyncBatches command
*/
class HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase
{
use ValidatesParameters;
public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)
{
parent::__construct($client);
}
/**
* Process webhook-collected deal batches.
*
* Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.
* Returns empty generator as deals are processed via jobs, not yielded directly.
*
* @param array $params Must contain 'config' key with Configuration instance
* @param int $total Output parameter - set to number of deal IDs dispatched
* @param string|null $lastRecordId Not used in webhook strategy
*
* @return \Generator Empty generator (deals processed via jobs)
*/
public function fetchOpportunities(
array $params,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$this->validateParameters($params);
$configId = $params['config']->getId();
$count = $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
$total = $count;
Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [
'config_id' => $configId,
'total_dispatched' => $total,
]);
yield from [];
}
public function validateParameters(array $params): bool
{
$this->validateBaseParameters($params);
return true;
}
protected function buildQuery(array $params, array $fields): array
{
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"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\\OpportunitySyncStrategy;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\Concerns\\ValidatesParameters;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\n\n/**\n * Webhook-driven batch sync strategy for HubSpot deals (opportunities).\n *\n * This strategy processes opportunities collected from webhook property change and\n * association change events. Deal CRM IDs are accumulated in Redis sets and processed\n * in batches via ImportOpportunityBatch jobs.\n *\n * Unlike traditional sync strategies that fetch deals from HubSpot API based on\n * modification dates or other filters, this strategy only processes deals that have\n * been explicitly flagged for sync via webhook events.\n *\n * Webhook Event Flow:\n * 1. Webhook event received (deal.propertyChange or deal.associationChange)\n * 2. Change Handler validates and collects deal ID\n * 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all\n * 4. This strategy drains Redis set during SyncObjects cycle\n * 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100\n *\n * Benefits:\n * - Near real-time sync (processes only changed deals)\n * - Reduced API calls (no polling for all deals)\n * - Automatic deduplication via Redis sets\n * - Resilient failure handling (failed IDs requeued to Redis)\n *\n * Usage:\n * - Automatically invoked during SyncObjects cycle for teams configured with this strategy\n * - Can be manually triggered via ProcessHubspotObjectsSyncBatches command\n */\nclass HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase\n{\n use ValidatesParameters;\n\n public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)\n {\n parent::__construct($client);\n }\n\n /**\n * Process webhook-collected deal batches.\n *\n * Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.\n * Returns empty generator as deals are processed via jobs, not yielded directly.\n *\n * @param array $params Must contain 'config' key with Configuration instance\n * @param int $total Output parameter - set to number of deal IDs dispatched\n * @param string|null $lastRecordId Not used in webhook strategy\n *\n * @return \\Generator Empty generator (deals processed via jobs)\n */\n public function fetchOpportunities(\n array $params,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $this->validateParameters($params);\n\n $configId = $params['config']->getId();\n $count = $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n\n $total = $count;\n\n Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [\n 'config_id' => $configId,\n 'total_dispatched' => $total,\n ]);\n\n yield from [];\n }\n\n public function validateParameters(array $params): bool\n {\n $this->validateBaseParameters($params);\n\n return true;\n }\n\n protected function buildQuery(array $params, array $fields): array\n {\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\Concerns\\ValidatesParameters;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\n\n/**\n * Webhook-driven batch sync strategy for HubSpot deals (opportunities).\n *\n * This strategy processes opportunities collected from webhook property change and\n * association change events. Deal CRM IDs are accumulated in Redis sets and processed\n * in batches via ImportOpportunityBatch jobs.\n *\n * Unlike traditional sync strategies that fetch deals from HubSpot API based on\n * modification dates or other filters, this strategy only processes deals that have\n * been explicitly flagged for sync via webhook events.\n *\n * Webhook Event Flow:\n * 1. Webhook event received (deal.propertyChange or deal.associationChange)\n * 2. Change Handler validates and collects deal ID\n * 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all\n * 4. This strategy drains Redis set during SyncObjects cycle\n * 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100\n *\n * Benefits:\n * - Near real-time sync (processes only changed deals)\n * - Reduced API calls (no polling for all deals)\n * - Automatic deduplication via Redis sets\n * - Resilient failure handling (failed IDs requeued to Redis)\n *\n * Usage:\n * - Automatically invoked during SyncObjects cycle for teams configured with this strategy\n * - Can be manually triggered via ProcessHubspotObjectsSyncBatches command\n */\nclass HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase\n{\n use ValidatesParameters;\n\n public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)\n {\n parent::__construct($client);\n }\n\n /**\n * Process webhook-collected deal batches.\n *\n * Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.\n * Returns empty generator as deals are processed via jobs, not yielded directly.\n *\n * @param array $params Must contain 'config' key with Configuration instance\n * @param int $total Output parameter - set to number of deal IDs dispatched\n * @param string|null $lastRecordId Not used in webhook strategy\n *\n * @return \\Generator Empty generator (deals processed via jobs)\n */\n public function fetchOpportunities(\n array $params,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $this->validateParameters($params);\n\n $configId = $params['config']->getId();\n $count = $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n\n $total = $count;\n\n Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [\n 'config_id' => $configId,\n 'total_dispatched' => $total,\n ]);\n\n yield from [];\n }\n\n public function validateParameters(array $params): bool\n {\n $this->validateBaseParameters($params);\n\n return true;\n }\n\n protected function buildQuery(array $params, array $fields): array\n {\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
4124530250875778275
|
-8600434810761384628
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\Concerns\ValidatesParameters;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
/**
* Webhook-driven batch sync strategy for HubSpot deals (opportunities).
*
* This strategy processes opportunities collected from webhook property change and
* association change events. Deal CRM IDs are accumulated in Redis sets and processed
* in batches via ImportOpportunityBatch jobs.
*
* Unlike traditional sync strategies that fetch deals from HubSpot API based on
* modification dates or other filters, this strategy only processes deals that have
* been explicitly flagged for sync via webhook events.
*
* Webhook Event Flow:
* 1. Webhook event received (deal.propertyChange or deal.associationChange)
* 2. Change Handler validates and collects deal ID
* 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all
* 4. This strategy drains Redis set during SyncObjects cycle
* 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100
*
* Benefits:
* - Near real-time sync (processes only changed deals)
* - Reduced API calls (no polling for all deals)
* - Automatic deduplication via Redis sets
* - Resilient failure handling (failed IDs requeued to Redis)
*
* Usage:
* - Automatically invoked during SyncObjects cycle for teams configured with this strategy
* - Can be manually triggered via ProcessHubspotObjectsSyncBatches command
*/
class HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase
{
use ValidatesParameters;
public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)
{
parent::__construct($client);
}
/**
* Process webhook-collected deal batches.
*
* Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.
* Returns empty generator as deals are processed via jobs, not yielded directly.
*
* @param array $params Must contain 'config' key with Configuration instance
* @param int $total Output parameter - set to number of deal IDs dispatched
* @param string|null $lastRecordId Not used in webhook strategy
*
* @return \Generator Empty generator (deals processed via jobs)
*/
public function fetchOpportunities(
array $params,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$this->validateParameters($params);
$configId = $params['config']->getId();
$count = $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
$total = $count;
Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [
'config_id' => $configId,
'total_dispatched' => $total,
]);
yield from [];
}
public function validateParameters(array $params): bool
{
$this->validateBaseParameters($params);
return true;
}
protected function buildQuery(array $params, array $fields): array
{
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder...
|
2760
|
NULL
|
NULL
|
NULL
|
|
2764
|
111
|
51
|
2026-05-07T11:39:18.961150+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153958961_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotWebhookBatchSyncStrategy.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\Concerns\ValidatesParameters;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
/**
* Webhook-driven batch sync strategy for HubSpot deals (opportunities).
*
* This strategy processes opportunities collected from webhook property change and
* association change events. Deal CRM IDs are accumulated in Redis sets and processed
* in batches via ImportOpportunityBatch jobs.
*
* Unlike traditional sync strategies that fetch deals from HubSpot API based on
* modification dates or other filters, this strategy only processes deals that have
* been explicitly flagged for sync via webhook events.
*
* Webhook Event Flow:
* 1. Webhook event received (deal.propertyChange or deal.associationChange)
* 2. Change Handler validates and collects deal ID
* 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all
* 4. This strategy drains Redis set during SyncObjects cycle
* 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100
*
* Benefits:
* - Near real-time sync (processes only changed deals)
* - Reduced API calls (no polling for all deals)
* - Automatic deduplication via Redis sets
* - Resilient failure handling (failed IDs requeued to Redis)
*
* Usage:
* - Automatically invoked during SyncObjects cycle for teams configured with this strategy
* - Can be manually triggered via ProcessHubspotObjectsSyncBatches command
*/
class HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase
{
use ValidatesParameters;
public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)
{
parent::__construct($client);
}
/**
* Process webhook-collected deal batches.
*
* Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.
* Returns empty generator as deals are processed via jobs, not yielded directly.
*
* @param array $params Must contain 'config' key with Configuration instance
* @param int $total Output parameter - set to number of deal IDs dispatched
* @param string|null $lastRecordId Not used in webhook strategy
*
* @return \Generator Empty generator (deals processed via jobs)
*/
public function fetchOpportunities(
array $params,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$this->validateParameters($params);
$configId = $params['config']->getId();
$count = $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
$total = $count;
Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [
'config_id' => $configId,
'total_dispatched' => $total,
]);
yield from [];
}
public function validateParameters(array $params): bool
{
$this->validateBaseParameters($params);
return true;
}
protected function buildQuery(array $params, array $fields): array
{
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"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\\OpportunitySyncStrategy;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\Concerns\\ValidatesParameters;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\n\n/**\n * Webhook-driven batch sync strategy for HubSpot deals (opportunities).\n *\n * This strategy processes opportunities collected from webhook property change and\n * association change events. Deal CRM IDs are accumulated in Redis sets and processed\n * in batches via ImportOpportunityBatch jobs.\n *\n * Unlike traditional sync strategies that fetch deals from HubSpot API based on\n * modification dates or other filters, this strategy only processes deals that have\n * been explicitly flagged for sync via webhook events.\n *\n * Webhook Event Flow:\n * 1. Webhook event received (deal.propertyChange or deal.associationChange)\n * 2. Change Handler validates and collects deal ID\n * 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all\n * 4. This strategy drains Redis set during SyncObjects cycle\n * 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100\n *\n * Benefits:\n * - Near real-time sync (processes only changed deals)\n * - Reduced API calls (no polling for all deals)\n * - Automatic deduplication via Redis sets\n * - Resilient failure handling (failed IDs requeued to Redis)\n *\n * Usage:\n * - Automatically invoked during SyncObjects cycle for teams configured with this strategy\n * - Can be manually triggered via ProcessHubspotObjectsSyncBatches command\n */\nclass HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase\n{\n use ValidatesParameters;\n\n public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)\n {\n parent::__construct($client);\n }\n\n /**\n * Process webhook-collected deal batches.\n *\n * Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.\n * Returns empty generator as deals are processed via jobs, not yielded directly.\n *\n * @param array $params Must contain 'config' key with Configuration instance\n * @param int $total Output parameter - set to number of deal IDs dispatched\n * @param string|null $lastRecordId Not used in webhook strategy\n *\n * @return \\Generator Empty generator (deals processed via jobs)\n */\n public function fetchOpportunities(\n array $params,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $this->validateParameters($params);\n\n $configId = $params['config']->getId();\n $count = $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n\n $total = $count;\n\n Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [\n 'config_id' => $configId,\n 'total_dispatched' => $total,\n ]);\n\n yield from [];\n }\n\n public function validateParameters(array $params): bool\n {\n $this->validateBaseParameters($params);\n\n return true;\n }\n\n protected function buildQuery(array $params, array $fields): array\n {\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy;\n\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\OpportunitySyncStrategy\\Concerns\\ValidatesParameters;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\n\n/**\n * Webhook-driven batch sync strategy for HubSpot deals (opportunities).\n *\n * This strategy processes opportunities collected from webhook property change and\n * association change events. Deal CRM IDs are accumulated in Redis sets and processed\n * in batches via ImportOpportunityBatch jobs.\n *\n * Unlike traditional sync strategies that fetch deals from HubSpot API based on\n * modification dates or other filters, this strategy only processes deals that have\n * been explicitly flagged for sync via webhook events.\n *\n * Webhook Event Flow:\n * 1. Webhook event received (deal.propertyChange or deal.associationChange)\n * 2. Change Handler validates and collects deal ID\n * 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all\n * 4. This strategy drains Redis set during SyncObjects cycle\n * 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100\n *\n * Benefits:\n * - Near real-time sync (processes only changed deals)\n * - Reduced API calls (no polling for all deals)\n * - Automatic deduplication via Redis sets\n * - Resilient failure handling (failed IDs requeued to Redis)\n *\n * Usage:\n * - Automatically invoked during SyncObjects cycle for teams configured with this strategy\n * - Can be manually triggered via ProcessHubspotObjectsSyncBatches command\n */\nclass HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase\n{\n use ValidatesParameters;\n\n public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)\n {\n parent::__construct($client);\n }\n\n /**\n * Process webhook-collected deal batches.\n *\n * Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.\n * Returns empty generator as deals are processed via jobs, not yielded directly.\n *\n * @param array $params Must contain 'config' key with Configuration instance\n * @param int $total Output parameter - set to number of deal IDs dispatched\n * @param string|null $lastRecordId Not used in webhook strategy\n *\n * @return \\Generator Empty generator (deals processed via jobs)\n */\n public function fetchOpportunities(\n array $params,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $this->validateParameters($params);\n\n $configId = $params['config']->getId();\n $count = $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,\n $configId\n );\n\n $total = $count;\n\n Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [\n 'config_id' => $configId,\n 'total_dispatched' => $total,\n ]);\n\n yield from [];\n }\n\n public function validateParameters(array $params): bool\n {\n $this->validateBaseParameters($params);\n\n return true;\n }\n\n protected function buildQuery(array $params, array $fields): array\n {\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
-9215504726697768214
|
-8312204297152901812
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
4
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy;
use Illuminate\Support\Facades\Log;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\OpportunitySyncStrategy\Concerns\ValidatesParameters;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
/**
* Webhook-driven batch sync strategy for HubSpot deals (opportunities).
*
* This strategy processes opportunities collected from webhook property change and
* association change events. Deal CRM IDs are accumulated in Redis sets and processed
* in batches via ImportOpportunityBatch jobs.
*
* Unlike traditional sync strategies that fetch deals from HubSpot API based on
* modification dates or other filters, this strategy only processes deals that have
* been explicitly flagged for sync via webhook events.
*
* Webhook Event Flow:
* 1. Webhook event received (deal.propertyChange or deal.associationChange)
* 2. Change Handler validates and collects deal ID
* 3. Deal ID added to Redis set: batch_sync_deal:{configId}:all
* 4. This strategy drains Redis set during SyncObjects cycle
* 5. ImportOpportunityBatch jobs dispatched to process deals in batches of 100
*
* Benefits:
* - Near real-time sync (processes only changed deals)
* - Reduced API calls (no polling for all deals)
* - Automatic deduplication via Redis sets
* - Resilient failure handling (failed IDs requeued to Redis)
*
* Usage:
* - Automatically invoked during SyncObjects cycle for teams configured with this strategy
* - Can be manually triggered via ProcessHubspotObjectsSyncBatches command
*/
class HubspotWebhookBatchSyncStrategy extends HubspotSyncStrategyBase
{
use ValidatesParameters;
public function __construct(Client $client, private WebhookSyncBatchProcessor $batchProcessor)
{
parent::__construct($client);
}
/**
* Process webhook-collected deal batches.
*
* Drains Redis sets containing deal CRM IDs and dispatches ImportOpportunityBatch jobs.
* Returns empty generator as deals are processed via jobs, not yielded directly.
*
* @param array $params Must contain 'config' key with Configuration instance
* @param int $total Output parameter - set to number of deal IDs dispatched
* @param string|null $lastRecordId Not used in webhook strategy
*
* @return \Generator Empty generator (deals processed via jobs)
*/
public function fetchOpportunities(
array $params,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$this->validateParameters($params);
$configId = $params['config']->getId();
$count = $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_DEAL,
$configId
);
$total = $count;
Log::info('[HubspotWebhookBatchSyncStrategy] Deal batches processed', [
'config_id' => $configId,
'total_dispatched' => $total,
]);
yield from [];
}
public function validateParameters(array $params): bool
{
$this->validateBaseParameters($params);
return true;
}
protected function buildQuery(array $params, array $fields): array
{
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2658
|
112
|
0
|
2026-05-07T11:34:22.187425+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153662187_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4441326695014905459
|
919851285442074724
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2656
|
NULL
|
NULL
|
NULL
|
|
2660
|
112
|
1
|
2026-05-07T11:34:25.670362+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153665670_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2694430278241490955
|
919851285978945636
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2662
|
112
|
2
|
2026-05-07T11:34:28.065252+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153668065_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8350884067385923069
|
919851285978945636
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2660
|
NULL
|
NULL
|
NULL
|
|
2664
|
112
|
3
|
2026-05-07T11:34:30.237276+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153670237_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
405275140667451668
|
919851285978945636
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2665
|
112
|
4
|
2026-05-07T11:34:38.942227+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153678942_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"bounds":{"left":0.22240691,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For search specifically, since headers don't come back, the local counter","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on 429 (you really did make that request).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with multiple jobs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Concurrency cap","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"via","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue::throttle()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Per-tenant fairness","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Priority lanes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-priority","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-bulk","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Backoff on 429","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— respect the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"header. If absent, exponential with jitter (e.g.,","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"min(2^attempt * 250ms, 30s) ± 20%","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with paginated requests","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with paginated requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The trap is treating \"fetch all\" as one logical operation. Each page is its own API call and competes for tokens with everything else.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two practical rules:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Don't hold the worker idle while paginating.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processing","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"of page N as a separate job, and queue a \"fetch page N+1\" job with a delay equal to the wait time. Each page becomes its own atomic unit.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Always batch where HubSpot offers it.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/{type}/batch/{read|update|create}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"accepts up to 100 IDs per call. For your \"patching IDs\" flow this is the difference between 600 calls and 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Walkthrough: 600 opportunities","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Walkthrough: 600 opportunities","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sane flow:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Identify deltas","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you can avoid Search at all, do. Use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"current state —","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 IDs per call → 6 calls. Burst weight 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compute diff","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in your code (no API).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 per call → 6 calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Associations","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v4/associations/{from}/{to}/batch/create","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if needed → another few batch calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Recognition: pre-check denies if","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"<weight>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"slots aren't free; on 429 the response surfaces","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(DAILY/SECONDLY) — log it, push the job back with","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"delay, increment a metric so you can alarm on sustained 429 rate.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Pitfalls & bottlenecks worth pre-empting","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Pitfalls & bottlenecks worth pre-empting","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily quota is shared across the whole portal","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search is uniquely fragile","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hs_object_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ranges or by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"windows.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry storms after HubSpot incidents","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhook → sync → webhook loops","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jy_last_sync_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") and ignore webhooks where it just changed.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Token refresh","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"expires_in - 60s","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", single-flight the refresh (only one worker refreshes; others wait on a lock).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search latency for fresh writes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Clock skew","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— Redis is authoritative. Don't use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"time()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from each worker for window scoring; use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis.call('TIME')","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inside the Lua script.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dead workers with held tokens","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The 5% error rule","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Phantom contention with Laravel","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited","depth":29,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"middleware","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": it's per-job-class by default, not per-portal. If your","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncDealJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateActivityJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotRateLimiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"service that everything routes through.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: What will happen if there are two different types of rate limits?","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: What will happen if there are two different types of rate limits?","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"What will happen if there are two different types of rate limits?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search requests that have 5 requests per second","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:40","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed independent rate limit buckets and their practical interactions","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed independent rate limit buckets and their practical interactions","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"They're independent buckets — with one shared bucket on top.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"The three buckets and what each call hits","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"The three buckets and what each call hits","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"┌──────────────────┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search request ──→ │ Search: 5/sec │ ──┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"├──→ ┌─────────────────┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"┌──────────────────┐ │ │ Daily: 250k–1M │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ └─────────────────┘","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What this means practically","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What this means practically","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting search secondly limit (5/s):","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only further","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting burst limit (190/10s):","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting daily limit:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"So can one type of request block the other?","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"So can one type of request block the other?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search → Burst:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst → Search:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating burst leaves Search's 5/s untouched.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Either → Daily:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", but only by contributing to the same daily counter. It's volume that kills you here, not the type.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In theory you can sustain ~50 search + ~190 other =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"~240 calls per 10 seconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"indefinitely (until daily runs out), because the two buckets bill independently.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Implementation consequence","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Implementation consequence","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'search'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for search endpoints only","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'burst'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for everything else","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'daily'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// every call, both kinds","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('search')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". A batch update calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('burst')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"One subtle pitfall: when you backoff on a 429,","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inspect","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in the response body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to know which bucket actually fired. If you see","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other subtlety: the burst response gives you","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: Ok, so what will happen each time I make a request?","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: Ok, so what will happen each time I make a request?","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:44","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What \"two buckets per request\" actually costs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What \"two buckets per request\" actually costs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Each request makes","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"one round trip to Redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lua","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[1] = burst bucket key (or search)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[2] = daily bucket key","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[1] = now (ms) ARGV[2] = window_ms","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[3] = bucket_max ARGV[4] = daily_max","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[5] = request_id ARGV[6] = daily_ttl","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- Trim sliding window","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZREMRANGEBYSCORE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"}]...
|
-8125974375056948043
|
-6913325059417482156
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=...
|
2664
|
NULL
|
NULL
|
NULL
|
|
2666
|
112
|
5
|
2026-05-07T11:34:41.903960+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153681903_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=
redis
.
call
(
'ZCARD'
,
KEYS
[
1
]
)
local
daily_used
=
tonumber
(
redis
.
call
(
'GET'
,
KEYS
[
2
]
)
or
'0'
)
if
burst_used
>=
tonumber
(
ARGV
[
3
]
)
then
-- Tell caller how long to sleep until oldest entry expires
local
oldest
=
redis
.
call
(
'ZRANGE'
,
KEYS
[
1
]
,
0
,
0
,
'WITHSCORES'
)
return
{
0
,
'BURST'
,
(
oldest
[
2
]
+
ARGV
[
2
]
)
-
ARGV
[
1
]
}
end
if
daily_used
>=
tonumber
(
ARGV
[
4
]
)
then
return
{
0
,
'DAILY'
,
-
1
}
end
redis
.
call
(
'ZADD'
,
KEYS
[
1
]
,
ARGV
[
1
]
,
ARGV
[
5
]
)
redis
.
call
(
'PEXPIRE'
,
KEYS
[
1
]
,
ARGV
[
2
]
+
1000
)
local
d
=
redis
.
call
(
'INCR'
,
KEYS
[
2
]
)
if
d
==
1
then
redis
.
call
(
'EXPIRE'
,
KEYS
[
2
]
,
ARGV
[
6
]
)
end
return
{
1
,
'OK'
,
tonumber
(
ARGV
[
3
]
)
-
burst_used
-
1
}
One
EVALSHA
call. Two keys touched. Returns either
{1, OK, remaining}
or...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"bounds":{"left":0.22240691,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For search specifically, since headers don't come back, the local counter","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on 429 (you really did make that request).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with multiple jobs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Concurrency cap","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"via","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue::throttle()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Per-tenant fairness","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Priority lanes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-priority","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-bulk","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Backoff on 429","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— respect the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"header. If absent, exponential with jitter (e.g.,","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"min(2^attempt * 250ms, 30s) ± 20%","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with paginated requests","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with paginated requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The trap is treating \"fetch all\" as one logical operation. Each page is its own API call and competes for tokens with everything else.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two practical rules:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Don't hold the worker idle while paginating.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processing","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"of page N as a separate job, and queue a \"fetch page N+1\" job with a delay equal to the wait time. Each page becomes its own atomic unit.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Always batch where HubSpot offers it.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/{type}/batch/{read|update|create}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"accepts up to 100 IDs per call. For your \"patching IDs\" flow this is the difference between 600 calls and 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Walkthrough: 600 opportunities","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Walkthrough: 600 opportunities","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sane flow:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Identify deltas","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you can avoid Search at all, do. Use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"current state —","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 IDs per call → 6 calls. Burst weight 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compute diff","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in your code (no API).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 per call → 6 calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Associations","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v4/associations/{from}/{to}/batch/create","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if needed → another few batch calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Recognition: pre-check denies if","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"<weight>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"slots aren't free; on 429 the response surfaces","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(DAILY/SECONDLY) — log it, push the job back with","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"delay, increment a metric so you can alarm on sustained 429 rate.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Pitfalls & bottlenecks worth pre-empting","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Pitfalls & bottlenecks worth pre-empting","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily quota is shared across the whole portal","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search is uniquely fragile","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hs_object_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ranges or by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"windows.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry storms after HubSpot incidents","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhook → sync → webhook loops","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jy_last_sync_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") and ignore webhooks where it just changed.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Token refresh","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"expires_in - 60s","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", single-flight the refresh (only one worker refreshes; others wait on a lock).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search latency for fresh writes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Clock skew","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— Redis is authoritative. Don't use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"time()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from each worker for window scoring; use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis.call('TIME')","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inside the Lua script.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dead workers with held tokens","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The 5% error rule","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Phantom contention with Laravel","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited","depth":29,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"middleware","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": it's per-job-class by default, not per-portal. If your","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncDealJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateActivityJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotRateLimiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"service that everything routes through.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: What will happen if there are two different types of rate limits?","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: What will happen if there are two different types of rate limits?","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"What will happen if there are two different types of rate limits?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search requests that have 5 requests per second","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:40","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed independent rate limit buckets and their practical interactions","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed independent rate limit buckets and their practical interactions","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"They're independent buckets — with one shared bucket on top.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"The three buckets and what each call hits","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"The three buckets and what each call hits","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"┌──────────────────┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search request ──→ │ Search: 5/sec │ ──┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"├──→ ┌─────────────────┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"┌──────────────────┐ │ │ Daily: 250k–1M │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ └─────────────────┘","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What this means practically","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What this means practically","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting search secondly limit (5/s):","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only further","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting burst limit (190/10s):","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting daily limit:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"So can one type of request block the other?","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"So can one type of request block the other?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search → Burst:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst → Search:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating burst leaves Search's 5/s untouched.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Either → Daily:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", but only by contributing to the same daily counter. It's volume that kills you here, not the type.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In theory you can sustain ~50 search + ~190 other =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"~240 calls per 10 seconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"indefinitely (until daily runs out), because the two buckets bill independently.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Implementation consequence","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Implementation consequence","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'search'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for search endpoints only","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'burst'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for everything else","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'daily'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// every call, both kinds","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('search')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". A batch update calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('burst')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"One subtle pitfall: when you backoff on a 429,","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inspect","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in the response body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to know which bucket actually fired. If you see","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other subtlety: the burst response gives you","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: Ok, so what will happen each time I make a request?","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: Ok, so what will happen each time I make a request?","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:44","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What \"two buckets per request\" actually costs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What \"two buckets per request\" actually costs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Each request makes","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"one round trip to Redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lua","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[1] = burst bucket key (or search)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[2] = daily bucket key","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[1] = now (ms) ARGV[2] = window_ms","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[3] = bucket_max ARGV[4] = daily_max","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[5] = request_id ARGV[6] = daily_ttl","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- Trim sliding window","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZREMRANGEBYSCORE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZCARD'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"daily_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'GET'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'0'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":">=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- Tell caller how long to sleep until oldest entry expires","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"oldest","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZRANGE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'WITHSCORES'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'BURST'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"oldest","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"daily_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":">=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZADD'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'PEXPIRE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1000","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"d","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'INCR'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"d","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"==","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'EXPIRE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'OK'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"One","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EVALSHA","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call. Two keys touched. Returns either","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{1, OK, remaining}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or","depth":26,"on_screen":false,"role_description":"text"}]...
|
3363610011336946299
|
-9219238470661317548
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=
redis
.
call
(
'ZCARD'
,
KEYS
[
1
]
)
local
daily_used
=
tonumber
(
redis
.
call
(
'GET'
,
KEYS
[
2
]
)
or
'0'
)
if
burst_used
>=
tonumber
(
ARGV
[
3
]
)
then
-- Tell caller how long to sleep until oldest entry expires
local
oldest
=
redis
.
call
(
'ZRANGE'
,
KEYS
[
1
]
,
0
,
0
,
'WITHSCORES'
)
return
{
0
,
'BURST'
,
(
oldest
[
2
]
+
ARGV
[
2
]
)
-
ARGV
[
1
]
}
end
if
daily_used
>=
tonumber
(
ARGV
[
4
]
)
then
return
{
0
,
'DAILY'
,
-
1
}
end
redis
.
call
(
'ZADD'
,
KEYS
[
1
]
,
ARGV
[
1
]
,
ARGV
[
5
]
)
redis
.
call
(
'PEXPIRE'
,
KEYS
[
1
]
,
ARGV
[
2
]
+
1000
)
local
d
=
redis
.
call
(
'INCR'
,
KEYS
[
2
]
)
if
d
==
1
then
redis
.
call
(
'EXPIRE'
,
KEYS
[
2
]
,
ARGV
[
6
]
)
end
return
{
1
,
'OK'
,
tonumber
(
ARGV
[
3
]
)
-
burst_used
-
1
}
One
EVALSHA
call. Two keys touched. Returns either
{1, OK, remaining}
or...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2667
|
112
|
6
|
2026-05-07T11:34:42.696988+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153682696_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"bounds":{"left":0.22240691,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For search specifically, since headers don't come back, the local counter","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on 429 (you really did make that request).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with multiple jobs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Concurrency cap","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"via","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue::throttle()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Per-tenant fairness","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Priority lanes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-priority","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-bulk","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Backoff on 429","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— respect the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"header. If absent, exponential with jitter (e.g.,","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"min(2^attempt * 250ms, 30s) ± 20%","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with paginated requests","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with paginated requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The trap is treating \"fetch all\" as one logical operation. Each page is its own API call and competes for tokens with everything else.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two practical rules:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Don't hold the worker idle while paginating.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processing","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"of page N as a separate job, and queue a \"fetch page N+1\" job with a delay equal to the wait time. Each page becomes its own atomic unit.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Always batch where HubSpot offers it.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/{type}/batch/{read|update|create}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"accepts up to 100 IDs per call. For your \"patching IDs\" flow this is the difference between 600 calls and 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Walkthrough: 600 opportunities","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Walkthrough: 600 opportunities","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sane flow:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Identify deltas","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you can avoid Search at all, do. Use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"current state —","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 IDs per call → 6 calls. Burst weight 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compute diff","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in your code (no API).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 per call → 6 calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Associations","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v4/associations/{from}/{to}/batch/create","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if needed → another few batch calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Recognition: pre-check denies if","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"<weight>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"slots aren't free; on 429 the response surfaces","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(DAILY/SECONDLY) — log it, push the job back with","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"delay, increment a metric so you can alarm on sustained 429 rate.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Pitfalls & bottlenecks worth pre-empting","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Pitfalls & bottlenecks worth pre-empting","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily quota is shared across the whole portal","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search is uniquely fragile","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hs_object_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ranges or by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"windows.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry storms after HubSpot incidents","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhook → sync → webhook loops","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jy_last_sync_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") and ignore webhooks where it just changed.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Token refresh","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"expires_in - 60s","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", single-flight the refresh (only one worker refreshes; others wait on a lock).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search latency for fresh writes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Clock skew","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— Redis is authoritative. Don't use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"time()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from each worker for window scoring; use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis.call('TIME')","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inside the Lua script.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dead workers with held tokens","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The 5% error rule","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Phantom contention with Laravel","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited","depth":29,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"middleware","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": it's per-job-class by default, not per-portal. If your","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncDealJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateActivityJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotRateLimiter","depth":28,"on_screen":false,"role_description":"text"}]...
|
7736829779516325445
|
3606538140289685596
|
click
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter...
|
2666
|
NULL
|
NULL
|
NULL
|
|
2669
|
112
|
7
|
2026-05-07T11:34:44.942977+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153684942_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=
redis
.
call
(
'ZCARD'
,
KEYS
[
1
]
)
local
daily_used
=
tonumber
(
redis
.
call
(
'GET'
,
KEYS
[
2
]
)
or
'0'
)
if
burst_used
>=
tonumber
(
ARGV
[
3
]
)
then
-- Tell caller how long to sleep until oldest entry expires
local
oldest
=
redis
.
call
(
'ZRANGE'
,
KEYS
[
1
]
,
0
,
0
,
'WITHSCORES'
)
return
{
0
,
'BURST'
,
(
oldest
[
2
]
+
ARGV
[
2
]
)
-
ARGV
[
1
]
}
end
if
daily_used
>=
tonumber
(
ARGV
[
4
]
)
then
return
{
0
,
'DAILY'
,
-
1
}
end
redis
.
call
(
'ZADD'
,
KEYS
[
1
]
,
ARGV
[
1
]
,
ARGV
[
5
]
)
redis
.
call
(
'PEXPIRE'
,
KEYS
[
1
]
,
ARGV
[
2
]
+
1000
)
local
d
=
redis
.
call
(
'INCR'
,
KEYS
[
2
]
)
if
d
==
1...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"bounds":{"left":0.22240691,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For search specifically, since headers don't come back, the local counter","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on 429 (you really did make that request).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with multiple jobs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Concurrency cap","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"via","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue::throttle()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Per-tenant fairness","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Priority lanes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-priority","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-bulk","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Backoff on 429","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— respect the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"header. If absent, exponential with jitter (e.g.,","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"min(2^attempt * 250ms, 30s) ± 20%","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with paginated requests","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with paginated requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The trap is treating \"fetch all\" as one logical operation. Each page is its own API call and competes for tokens with everything else.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two practical rules:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Don't hold the worker idle while paginating.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processing","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"of page N as a separate job, and queue a \"fetch page N+1\" job with a delay equal to the wait time. Each page becomes its own atomic unit.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Always batch where HubSpot offers it.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/{type}/batch/{read|update|create}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"accepts up to 100 IDs per call. For your \"patching IDs\" flow this is the difference between 600 calls and 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Walkthrough: 600 opportunities","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Walkthrough: 600 opportunities","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sane flow:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Identify deltas","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you can avoid Search at all, do. Use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"current state —","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 IDs per call → 6 calls. Burst weight 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compute diff","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in your code (no API).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 per call → 6 calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Associations","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v4/associations/{from}/{to}/batch/create","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if needed → another few batch calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Recognition: pre-check denies if","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"<weight>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"slots aren't free; on 429 the response surfaces","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(DAILY/SECONDLY) — log it, push the job back with","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"delay, increment a metric so you can alarm on sustained 429 rate.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Pitfalls & bottlenecks worth pre-empting","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Pitfalls & bottlenecks worth pre-empting","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily quota is shared across the whole portal","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search is uniquely fragile","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hs_object_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ranges or by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"windows.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry storms after HubSpot incidents","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhook → sync → webhook loops","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jy_last_sync_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") and ignore webhooks where it just changed.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Token refresh","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"expires_in - 60s","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", single-flight the refresh (only one worker refreshes; others wait on a lock).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search latency for fresh writes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Clock skew","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— Redis is authoritative. Don't use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"time()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from each worker for window scoring; use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis.call('TIME')","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inside the Lua script.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dead workers with held tokens","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The 5% error rule","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Phantom contention with Laravel","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited","depth":29,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"middleware","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": it's per-job-class by default, not per-portal. If your","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncDealJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateActivityJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotRateLimiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"service that everything routes through.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: What will happen if there are two different types of rate limits?","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: What will happen if there are two different types of rate limits?","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"What will happen if there are two different types of rate limits?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search requests that have 5 requests per second","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:40","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed independent rate limit buckets and their practical interactions","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed independent rate limit buckets and their practical interactions","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"They're independent buckets — with one shared bucket on top.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"The three buckets and what each call hits","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"The three buckets and what each call hits","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"┌──────────────────┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search request ──→ │ Search: 5/sec │ ──┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"├──→ ┌─────────────────┐","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"┌──────────────────┐ │ │ Daily: 250k–1M │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"└──────────────────┘ └─────────────────┘","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What this means practically","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What this means practically","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting search secondly limit (5/s):","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only further","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting burst limit (190/10s):","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hitting daily limit:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"So can one type of request block the other?","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"So can one type of request block the other?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search → Burst:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst → Search:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saturating burst leaves Search's 5/s untouched.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Either → Daily:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", but only by contributing to the same daily counter. It's volume that kills you here, not the type.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In theory you can sustain ~50 search + ~190 other =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"~240 calls per 10 seconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"indefinitely (until daily runs out), because the two buckets bill independently.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Implementation consequence","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Implementation consequence","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'search'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for search endpoints only","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'burst'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// for everything else","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$limiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'daily'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$weight","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// every call, both kinds","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('search')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". A batch update calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('burst')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"acquire('daily')","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"One subtle pitfall: when you backoff on a 429,","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inspect","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in the response body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to know which bucket actually fired. If you see","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other subtlety: the burst response gives you","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: Ok, so what will happen each time I make a request?","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: Ok, so what will happen each time I make a request?","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:44","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzed per-request overhead concerns and quantified Redis performance impact","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What \"two buckets per request\" actually costs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What \"two buckets per request\" actually costs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Each request makes","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"one round trip to Redis","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"lua","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[1] = burst bucket key (or search)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- KEYS[2] = daily bucket key","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[1] = now (ms) ARGV[2] = window_ms","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[3] = bucket_max ARGV[4] = daily_max","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- ARGV[5] = request_id ARGV[6] = daily_ttl","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- Trim sliding window","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZREMRANGEBYSCORE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZCARD'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"daily_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'GET'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'0'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"burst_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":">=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-- Tell caller how long to sleep until oldest entry expires","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"oldest","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZRANGE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'WITHSCORES'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'BURST'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"oldest","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"daily_used","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":">=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"tonumber","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"then","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"end","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'ZADD'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'PEXPIRE'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ARGV","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1000","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"local","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"d","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"call","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'INCR'","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KEYS","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"d","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"==","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":28,"on_screen":false,"role_description":"text"}]...
|
-4313505042667386954
|
-8642777718357926828
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions
Analyzed independent rate limit buckets and their practical interactions
They're independent buckets — with one shared bucket on top.
The three buckets and what each call hits
The three buckets and what each call hits
Copy to clipboard
┌──────────────────┐
Search request ──→ │ Search: 5/sec │ ──┐
└──────────────────┘ │
├──→ ┌─────────────────┐
┌──────────────────┐ │ │ Daily: 250k–1M │
Non-search call ──→ │ Burst: 190/10s │ ──┘ │ (per portal) │
└──────────────────┘ └─────────────────┘
A search call is only metered against the search bucket and the daily bucket. A non-search call (batch read/update, single PATCH, associations, etc.) is only metered against the burst bucket and the daily bucket. The two short-window buckets do not share counters.
What this means practically
What this means practically
Hitting search secondly limit (5/s):
Only further
/search
calls 429. Your batch reads, batch updates, single PATCHes, association calls — all keep working at full burst speed. This is actually useful: if your sync is bottlenecked on Search, you can pivot to using
lastmodifieddate
filters via batch endpoints, or use the IDs you already have to do batch reads, while Search cools off.
Hitting burst limit (190/10s):
Only non-search calls 429. You can still issue Search calls at 5/s. Useful for: continuing to identify deltas for queueing while in-flight updates drain, or refreshing a small set of records via Search even though your write pipeline is paused.
Hitting daily limit:
Everything stops. Search and non-search alike. Doesn't unblock until midnight in your portal's configured timezone — and remember this is shared across every private app in the portal, plus n8n flows, marketing tools, anything else hitting the API with that account.
So can one type of request block the other?
So can one type of request block the other?
Search → Burst:
No.
Saturating Search at 50 calls per 10s leaves your full 190/10s burst untouched.
Burst → Search:
No.
Saturating burst leaves Search's 5/s untouched.
Either → Daily:
Yes
, but only by contributing to the same daily counter. It's volume that kills you here, not the type.
In theory you can sustain ~50 search + ~190 other =
~240 calls per 10 seconds
indefinitely (until daily runs out), because the two buckets bill independently.
Implementation consequence
Implementation consequence
Your rate limiter needs to know which bucket a request hits before it acquires. Cleanest pattern:
Copy to clipboard
php
$limiter
->
acquire
(
'search'
,
$weight
=
1
)
;
// for search endpoints only
$limiter
->
acquire
(
'burst'
,
$weight
=
1
)
;
// for everything else
$limiter
->
acquire
(
'daily'
,
$weight
=
1
)
;
// every call, both kinds
In Redis: three separate keys, three separate Lua acquire scripts (or one script with a bucket parameter). A search call calls
acquire('search')
+
acquire('daily')
. A batch update calls
acquire('burst')
+
acquire('daily')
.
One subtle pitfall: when you backoff on a 429,
inspect
policyName
in the response body
to know which bucket actually fired. If you see
SECONDLY
from a search endpoint, you only need to delay search-bound jobs — burst-bound jobs in the queue can keep running. If you blanket-pause everything on any 429, you waste capacity on the bucket that's still healthy. Two queues (or two throttle keys) make this clean; one global pause is the lazy version.
The other subtlety: the burst response gives you
X-HubSpot-RateLimit-Remaining
to clamp local state against, but search doesn't return those headers. So your search counter is unverified — be slightly more conservative there (e.g., budget 4/s instead of 5) to absorb the unaccounted-for calls.
Copy
Give positive feedback
Give negative feedback
Retry
You said: Ok, so what will happen each time I make a request?
You said: Ok, so what will happen each time I make a request?
Ok, so what will happen each time I make a request? Let's say I have 100,000 requests to make that are instant. Do I need to increment each request based on the type? For example, if there is a search request, I need to increment both the search bucket and also the daily bucket. If it's a burst, then I can go to the daily and burst buckets. If I need to check if it can make a call, do I need to see both the daily and search, or burst, respectively? Isn't that too heavy for the infrastructure, adding additional load to each request?
13:44
Retry
Edit
Copy
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Claude responded: Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out.
Analyzed per-request overhead concerns and quantified Redis performance impact
Analyzed per-request overhead concerns and quantified Redis performance impact
Yes, your understanding is correct — every request must check and reserve from each bucket it applies to, atomically, before the call goes out. But the overhead is much smaller than it sounds, and importantly, it's nowhere near your bottleneck.
What "two buckets per request" actually costs
What "two buckets per request" actually costs
Each request makes
one round trip to Redis
— not two — because you fold both bucket checks into a single Lua script that runs atomically server-side:
Copy to clipboard
lua
-- KEYS[1] = burst bucket key (or search)
-- KEYS[2] = daily bucket key
-- ARGV[1] = now (ms) ARGV[2] = window_ms
-- ARGV[3] = bucket_max ARGV[4] = daily_max
-- ARGV[5] = request_id ARGV[6] = daily_ttl
-- Trim sliding window
redis
.
call
(
'ZREMRANGEBYSCORE'
,
KEYS
[
1
]
,
0
,
ARGV
[
1
]
-
ARGV
[
2
]
)
local
burst_used
=
redis
.
call
(
'ZCARD'
,
KEYS
[
1
]
)
local
daily_used
=
tonumber
(
redis
.
call
(
'GET'
,
KEYS
[
2
]
)
or
'0'
)
if
burst_used
>=
tonumber
(
ARGV
[
3
]
)
then
-- Tell caller how long to sleep until oldest entry expires
local
oldest
=
redis
.
call
(
'ZRANGE'
,
KEYS
[
1
]
,
0
,
0
,
'WITHSCORES'
)
return
{
0
,
'BURST'
,
(
oldest
[
2
]
+
ARGV
[
2
]
)
-
ARGV
[
1
]
}
end
if
daily_used
>=
tonumber
(
ARGV
[
4
]
)
then
return
{
0
,
'DAILY'
,
-
1
}
end
redis
.
call
(
'ZADD'
,
KEYS
[
1
]
,
ARGV
[
1
]
,
ARGV
[
5
]
)
redis
.
call
(
'PEXPIRE'
,
KEYS
[
1
]
,
ARGV
[
2
]
+
1000
)
local
d
=
redis
.
call
(
'INCR'
,
KEYS
[
2
]
)
if
d
==
1...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2670
|
112
|
8
|
2026-05-07T11:34:54.031073+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153694031_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7713698666319192685
|
919851285978945636
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2669
|
NULL
|
NULL
|
NULL
|
|
2672
|
112
|
9
|
2026-05-07T11:35:06.777768+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153706777_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true); ' . PHP_EOL . print_r($max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true); ' . PHP_EOL . print_r($max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true);, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true); ' . PHP_EOL . print_r($max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n$body = json_decode((string) $response->getBody(), true);, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3533231559131076087
|
919851285978945636
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true); ' . PHP_EOL . print_r($max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2674
|
112
|
10
|
2026-05-07T11:35:11.351056+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153711351_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7293938723963314954
|
919851285978945636
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2672
|
NULL
|
NULL
|
NULL
|
|
2676
|
112
|
11
|
2026-05-07T11:35:16.805009+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153716805_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3941099390594143240
|
919851285978945636
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2678
|
112
|
12
|
2026-05-07T11:35:20.464831+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153720464_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9016600555141330209
|
2072772790585792612
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2676
|
NULL
|
NULL
|
NULL
|
|
2680
|
112
|
13
|
2026-05-07T11:35:24.458223+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153724458_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7362719360473346427
|
6684458809013180516
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2681
|
112
|
14
|
2026-05-07T11:35:28.151571+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153728151_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8137451649170895720
|
6684458809013180516
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2680
|
NULL
|
NULL
|
NULL
|
|
2684
|
112
|
15
|
2026-05-07T11:35:31.277884+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153731277_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3585960046888772555
|
5531537304406333540
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2686
|
112
|
16
|
2026-05-07T11:35:34.463534+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153734463_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n \n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6582720202572488457
|
5531537304406333540
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2684
|
NULL
|
NULL
|
NULL
|
|
2689
|
112
|
17
|
2026-05-07T11:35:43.474640+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153743474_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-943456739835505562
|
-8636494053633454656
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
PhostormINavigarecodeLaravelFV faVsco.jsProledey© BatchSyncCollector.|Co kematchactviyoncrmoojectbetach.ong© RateLimitException.php© SyncToUserPilot.php© balchsynckealsservo clientonp© ClosedDealStagesSe © Middleware/RateLimited.phgo DealFieldsService.phDecorateAcuiviy.ong© FieldDefinitions.phpC) ProviderRateLimiter.phpC) FieldT vpeconvertenclass Cuient extends BasecLient imolements Hubspotc ientinterfacee HubspotclientintertaC) HubspotTokenManaC) PavloadBuilder.phpC) RemotecrmObiectMP ResponseNormalize.C) Service, ono@ SvncFieldAction.ohnC) SvncRelatedActivitvi 699C) WebhookSvncBatchlv D IntegrationAppMAccescorsMAn> D Config> DDTO> D Filters• M Jobs• D ProspectSearchStrat• D ServiceTraits© DataClient.php© DecorateActivity.phc© LocalSearch.phpLocalSearchInterface© RemoteSearch.php709711c) Service.phpv C Listenersc) ConvertLeadActivitiec) PurceLookuocache.li715> → Metadata> = Miaration• → Pioedrivev Salesforce> D FieldsM OnnortunitvMatcheM OnnortunitvSvneStral* dreturn arrau<crmFieldoptzon>publac function fetchupportun1tvF1elduptions(Fleld Sfleld: arrav-...* athrows BadReauest* athrows HubsnotExcentionpublic function makeRequest(string Sendpoint, Smethod = 'GET', $payload = [J, ?string $querystrSendpoint = self::BASE_URL • $endpoint;i+ Smethod ee= IGFT!)dreturn Sthis->getInstance()->getClientO2->request(Sresponse = Sthis->qetInstance(->qetClientO?->request(endpoint: sendpointquery string: squerystring} else {return Sthis->getInstance->getClient->request($method, Sendpoint, [Sresnonse = Sthis->aetInstance@->aetClient@->request(method.Sendnoint.n"ison' => (Snavload)I1):AccagtReiectSmax= Sresponse->getHeaderLine('X-HubSpot-RateLimit-Max'):Sremaining = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Remaining'):$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'): // "10k$body= ison_ decode((string) Sresponse->getBodyOassociative: tnue)•> M ProsnectSearchStrat> M ServiceTraitsC) Client nhr@ DecorateActivity.php. Delete@biectsTrait.n\Illuminate\Support\Facades\Log::channel( channel: 'custom_channel')->info('Smax" . PHP EQ\Illuminate\Support\Facades\Log::channel( channel'custom channel')->info('SremainingPH 73\Illuminate\Support\Facades\Log::channel( channel:Iauetom Ahonnoli)inGaftCintonuol)PHFuluminace supporc racades Loq::channel channel'custom channel')->info('$bodylPHP- EQL727return sresponse:© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.php730* Ethcows BadRequestts +X Reject File 0 * €* 2 0f4 files →arQube for IDE suggestions: Detect more security issues in your PHP files // Try SonarQube Cloud for free // Download SonarQube Server // Learn more // Don't ask again (today 10:25)Support Daily - in 25 m100% 12Thu 7 May 14:35:43AskJiminnyReportActivityServiceTest -+0 ..= custom.log= laravel.logA SF jiminny@localhost]« console [PROD]& console (EU]cascadeA console [STAGING]Hubspot Rate LimitindTx: Auto vdojiminny vsth1s->makeremobucketda1ly'. 250000, 864661.06 41 Y6 ^SELEC * FROM crm Field data WHERE crm lavout entity 1d = 9715SELEC * FROM crm Field data WHERE crm lavout entity id IN (6494.6495.6496.6497.6498.649995SELECTCONCAT(u.id, CASE WHEN V.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idWhat haopens it No rate limits are ser? ((Detault benavior)u.emailsa.*,t.owner_id FROM social_accounts saIOTN UserS Uon u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_idWHERE U.team id = 2 and sa.provider = 'hubspot':Looking at current code:php// ProviderRateLimiter:: canMakeRequestforeach (Sprovider->getRateLimitso as SrateLimit) <if ($this=>rateLimiter->tooManyAttempts(...)) return false;return true:/* If no rate Limits. returns TRUE (allows request)select * from social accounts where id = 1499:/ ProviderRateLimiter::incrementRequestCountforeach (Sorovider-saetRateLimits") as Sratelimit)select * from opportunities where team id = 2and crm_provider id IN ('51317301383'):// - If no rate limits, does NOTHING (no counters incremented)select * trom contacrs where 10 = 851select * from opportunities where team 1d = 2 order by 1d desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112|// ProviderRateLimiter:.requestAvailableTnreturn Sprovider->getRateLimits()->isNotEmpty()? Sprovider->getRateLimits()->map(...)->max()It no rate Limits,returns bselect * from opportunities where team_id = 2 and crm_provider_id = 55976759904; # 5112select * from opportunity contacts where opportunity 1d = 517current derault benavior.select * from crm_field_data where object_id = 1365SELEC * FROM crm FieldsWHERE 1O IN 01405.1407. 1912, 2128)9• V Request is allowed (no throttling)• V No Redis counters incremented•• retryAfter = 0 secondsselect * from features:select * from team_features where team_id IN (1):select * from team features where feature id TN ((36).This means: If vou don't manuallv seed rate limits rows for a HubSoot Contiauration, reauests ao through unthrottled.This is probably NOT what you want for production. You should:1. Create a seeder to add default rate limits for all existing HubSpot configurations2. Add to Confiquration creation flow to auto-create defaultsSHOW CREATE TABLE opportunity_contacts;SELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751' ;3. Or implement hardcoded fallbacks in code (Option 3 above)# $slug = 'HUBSPOT_WEBHOOK_SYNC':# $team = Jiminny \Models\Team::find(2):# $feature = Feature::query(->where('slug', $slug)->firstO:# TeamFeature::query->create(['feature id' => $feature-›getId,'team id' => $team->getIdO]):Mv PecommendationImmediate implementation (for testing)1. Use Option 3 (hybrid) - hardcoded defaults with DB override capabilit.2. Modifv executeRequest to accent <reauestTvne narameter# hubspot webhook metricsapp/Jobs/Middleware/ D HandleRateLimit.php +42ann/lohelCrm/Doloto/ DolotoCrmEntitvTrait nhn t18select * from opportunities where team id = 2 and crm_provider_ id IN ('374720564' '14527423589' '49908861993SELECT * FROM opportunity contacts WHERE opportunity_ id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501'•s/M RateLimitExcention.ohnReiect alllselect * from contacts where 1d in 1414Ask anvthing (&+bselect * From activitles where crm confiauration 1d = 2:<> CodeC Adantivecalant esttinaa fnam anm annfinunationW Windsurf Toams 2 carote UTF.8Accent alliio 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2691
|
112
|
18
|
2026-05-07T11:35:47.276872+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153747276_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.38297874,"top":0.09896249,"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":"Explain Plan","depth":4,"bounds":{"left":0.39162233,"top":0.09896249,"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":"Browse Query History","depth":4,"bounds":{"left":0.40259308,"top":0.09896249,"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":"View Parameters","depth":4,"bounds":{"left":0.4112367,"top":0.09896249,"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 Query Execution Settings…","depth":4,"bounds":{"left":0.41988033,"top":0.09896249,"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":"In-Editor Results","depth":4,"bounds":{"left":0.43085107,"top":0.09896249,"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":"Tx: Auto","depth":4,"bounds":{"left":0.4418218,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.46841756,"top":0.09896249,"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":"Playground","depth":4,"bounds":{"left":0.4793883,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.65359044,"top":0.09896249,"width":0.02825798,"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":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6399601,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.6499335,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.65924203,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.66888297,"top":0.12210695,"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.6761968,"top":0.12210695,"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":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
860006888846957654
|
2072772790585792612
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2689
|
NULL
|
NULL
|
NULL
|
|
2693
|
112
|
19
|
2026-05-07T11:35:50.823457+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153750823_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4800532,"height":-0.06304872},"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
6952889929047603526
|
4792633381515062291
|
click
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2695
|
112
|
20
|
2026-05-07T11:36:00.967010+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153760967_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormINavigarecodeLaravelProledeybatchsynccolle PhostormINavigarecodeLaravelProledeybatchsynccollector.© SyncToUserPilot.php© balchsynckealsservo clientonpc closeaDealstagesseMiddleware/RateLimited.ongo DealFieldsService.phDecorateAcuiviv.ont© FieldDefinitions.phpC) ProviderRateLimiter.phpC) FieldT vpeconvertenclass Cuient extends BasecLient imolements Hubspotcuientinterface@ HubspotClientinterfaC) HubspotTokenManaC) PavloadBuilder.phpC) RemotecrmObiectMP ResponseNormalize.C) Service, ono@ SvncFieldAction.ohnC) SvncRelatedActivitvi@ WebhookSvncBatchlv D IntegrationAppMAccescorsMAn• Config> DDTO> D Filters> m Jobs• D ProspectSearchStrat• M ServiceTraits© DataClient.php© DecorateActivity.phr 711© LocalSearch.phg© LocalSearchInterface713© RemoteSearch.php714c) Service.phpv C Listeners716c) ConvertLeadActivitiec) PurceLookuocache.li> → Metadata> D Miaration• → Pioedrivev Salesforce> D FieldsM OnnortunitvMatcheM OnnortunitvSvneStral> M ProsnectSearchStrat> M ServiceTraitsC) Client nhr@ DecorateActivity.php. Delete@biectsTrait.n© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.php* drexum arraysermrielduption>public function fetchOpportunityFieldOptions(Field $field): array{...}* othrows HubspotExceptionpublic function makeRequest(string Sendpoint. Smethod = 'GET'. Spavload = []. Pstring SauerystnSendooint = self::BASE URL sendooint:"GET")<Sresnonse = Sthis->aetinstanceo->aetcliento2->requestdendnoint: Sendoo.nt.query_string: $queryString3else {iSresponse = $this->getInstance)->getClient->request($method, $endpoint, ['json' => ($payload)Smax= Sresponse->getHeaderLine('X-HubSpot-RateLimit-Max'):Sremaining = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Remaining'):1/ "110"I/ "109"Sinterval = Sresponse->getHeaderLine(*X-HubSpot-RateLimit-Intenval-Miltiseconds"); // "109 6= 1son decode((string) Sresponse->qetBodyoassociative: true)ILLuminate Support Facades Loq::channel channelcustom_channel')->1nfo'Smax"Illuminate\Support\Facades\ Loq::channel( channel'custom_channel')->info('Sremaining 'PH 76UuuLumanate Suobort Facades Loa: :channet channel'custom channel")»>info( SintervailuuuLumanate Suobort Facades Loa: :channet channe'custom channel')->info('$bod›734 GVpublic function createMeeting(array $payload): Responsef...}AskJiminnyReportActivityServiceTest -Inu / May 14.30.00+0 ..= custom.log= laravel..gg x 4 SF (jiminny@localhosti& console [PROD]& console (EU]CascadeA console [STAGING]Hubspot Rate LimitindTx: Auto vdojiminny vsth1s->makeremobucketda1ly'. 250000, 864661.06 41 Y6 ^SELEC * FROM crm Field data WHERE crm lavout entity 1d = 9715SELEC * FROM crm Field data WHERE crm lavout entity id IN (6494.6495.6496.6497.6498.649995SELECTCONCAT(u.id, CASE WHEN V.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idu.emailsa.*,t.owner_id FROM social_accounts saIOTN UserS Uon u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_idWHERE U.team id = 2 and sa.provider = 'hubspot':select * from social accounts where id = 1499:select * from opportunities where team id = 2and crm_provider id IN ('51317301383'):select * trom contacrs where 10 = 851select * from opportunities where team 1d = 2 order by 1d desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112|select * from opportunities where team_id = 2 and crm_provider_id = 55976759904; # 5112select * from opportunity contacts where opportunity 1d = 57.select * from crm_field_data where object_id = 1365;SELEC * FROM crm FieldsWHERE 1O IN 01405. 1407. 1912, 212899select * fromselect * from team_features where team_id IN (1):select * from team features where feature id TN ((36).SHOW CREATE TABLE opportunity_contacts;SELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751' ;# $slug = 'HUBSPOT_WEBHOOK_SYNC':# $team = Jiminny \Models\Team::find(2):# $feature = Feature::query(->where('slug', $slug)->firstO:# TeamFeature::query)->create(['feature id' => $feature-›getId,'team id' => $team->getIdO]):What haopens it No rate limits are ser? ((Detault benavior)Looking at current code:php// ProviderRateLimiter:: canMakeRequestforeach (Sprovider->getRateLimitso as SrateLimit) <if ($this=>rateLimiter->tooManyAttempts(...)) return false;return true:• If no rate Limits. returns TRUE (allows request)/ ProviderRateLimiter::incrementRequestCountforeach (Sorovider-saetRateLimits") as Sratelimit)// - If no rate limits, does NOTHING (no counters incremented)// ProviderRateLimniter:.requestAvailableTnreturn Sprovider->getRateLimits()->isNotEmpty()? Sprovider->getRateLimits()->map(...)->max()It no rate Limits,returns bcurrent derault benavior.• V Request is allowed (no throttling)• V No Redis counters incrementedWrotruattor = 0 cosonddThis means: If vou don't manuallv seed rate limits rows for a HubSoot Contiauration, reauests ao through unthrottled.This is probably NOT what you want for production. You should:1. Create a seeder to add default rate limits for all existing HubSpot configurations2. Add to Contiquration creation flow to auto-create defaults3. Or implement hardcoded fallbacks in code (Option 3 above)Mv PecommendationImmediate implementation (for testing)1. Use Option 3 (hybrid) - hardcoded defaults with DB override capabilit.2. Modifv executeRequest to accent <requestTvne narameter# hubspot webhook metricsapp/Jobs/Middleware/ D HandleRateLimit.php +42ann/lohelCrm/Doloto/ DolotoCrmEntitvTrait nhn t18select * from opportunities where team id = 2 and crm_provider_ id IN ('374720564' '14527423589' '49908861993|SELECT * FROM opportunity contacts WHERE opportunity_ id = '414'.SELECT * FROM opportunity contacts WHERE crm_provider id = '131501'•select * from contacts where id in (414)s/M RateLimitExcention.ohnAsk anvthing (&+bselect * From activitles where crm confiauration 1d = 2:<> CodeC AdantiveReiect alllW Windsurf Toams 716-1UTF.8IAccent allfo 4 spaces...
|
NULL
|
2119170041890749132
|
NULL
|
click
|
ocr
|
NULL
|
PhostormINavigarecodeLaravelProledeybatchsynccolle PhostormINavigarecodeLaravelProledeybatchsynccollector.© SyncToUserPilot.php© balchsynckealsservo clientonpc closeaDealstagesseMiddleware/RateLimited.ongo DealFieldsService.phDecorateAcuiviv.ont© FieldDefinitions.phpC) ProviderRateLimiter.phpC) FieldT vpeconvertenclass Cuient extends BasecLient imolements Hubspotcuientinterface@ HubspotClientinterfaC) HubspotTokenManaC) PavloadBuilder.phpC) RemotecrmObiectMP ResponseNormalize.C) Service, ono@ SvncFieldAction.ohnC) SvncRelatedActivitvi@ WebhookSvncBatchlv D IntegrationAppMAccescorsMAn• Config> DDTO> D Filters> m Jobs• D ProspectSearchStrat• M ServiceTraits© DataClient.php© DecorateActivity.phr 711© LocalSearch.phg© LocalSearchInterface713© RemoteSearch.php714c) Service.phpv C Listeners716c) ConvertLeadActivitiec) PurceLookuocache.li> → Metadata> D Miaration• → Pioedrivev Salesforce> D FieldsM OnnortunitvMatcheM OnnortunitvSvneStral> M ProsnectSearchStrat> M ServiceTraitsC) Client nhr@ DecorateActivity.php. Delete@biectsTrait.n© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.php* drexum arraysermrielduption>public function fetchOpportunityFieldOptions(Field $field): array{...}* othrows HubspotExceptionpublic function makeRequest(string Sendpoint. Smethod = 'GET'. Spavload = []. Pstring SauerystnSendooint = self::BASE URL sendooint:"GET")<Sresnonse = Sthis->aetinstanceo->aetcliento2->requestdendnoint: Sendoo.nt.query_string: $queryString3else {iSresponse = $this->getInstance)->getClient->request($method, $endpoint, ['json' => ($payload)Smax= Sresponse->getHeaderLine('X-HubSpot-RateLimit-Max'):Sremaining = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Remaining'):1/ "110"I/ "109"Sinterval = Sresponse->getHeaderLine(*X-HubSpot-RateLimit-Intenval-Miltiseconds"); // "109 6= 1son decode((string) Sresponse->qetBodyoassociative: true)ILLuminate Support Facades Loq::channel channelcustom_channel')->1nfo'Smax"Illuminate\Support\Facades\ Loq::channel( channel'custom_channel')->info('Sremaining 'PH 76UuuLumanate Suobort Facades Loa: :channet channel'custom channel")»>info( SintervailuuuLumanate Suobort Facades Loa: :channet channe'custom channel')->info('$bod›734 GVpublic function createMeeting(array $payload): Responsef...}AskJiminnyReportActivityServiceTest -Inu / May 14.30.00+0 ..= custom.log= laravel..gg x 4 SF (jiminny@localhosti& console [PROD]& console (EU]CascadeA console [STAGING]Hubspot Rate LimitindTx: Auto vdojiminny vsth1s->makeremobucketda1ly'. 250000, 864661.06 41 Y6 ^SELEC * FROM crm Field data WHERE crm lavout entity 1d = 9715SELEC * FROM crm Field data WHERE crm lavout entity id IN (6494.6495.6496.6497.6498.649995SELECTCONCAT(u.id, CASE WHEN V.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_idu.emailsa.*,t.owner_id FROM social_accounts saIOTN UserS Uon u.id = sa.sociable_idJOIN teams t 1..n<->1: on t.id = u.team_idWHERE U.team id = 2 and sa.provider = 'hubspot':select * from social accounts where id = 1499:select * from opportunities where team id = 2and crm_provider id IN ('51317301383'):select * trom contacrs where 10 = 851select * from opportunities where team 1d = 2 order by 1d desc:select * from opportunities where team id = 2 and crm provider id = '51317301383': # 5112|select * from opportunities where team_id = 2 and crm_provider_id = 55976759904; # 5112select * from opportunity contacts where opportunity 1d = 57.select * from crm_field_data where object_id = 1365;SELEC * FROM crm FieldsWHERE 1O IN 01405. 1407. 1912, 212899select * fromselect * from team_features where team_id IN (1):select * from team features where feature id TN ((36).SHOW CREATE TABLE opportunity_contacts;SELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751' ;# $slug = 'HUBSPOT_WEBHOOK_SYNC':# $team = Jiminny \Models\Team::find(2):# $feature = Feature::query(->where('slug', $slug)->firstO:# TeamFeature::query)->create(['feature id' => $feature-›getId,'team id' => $team->getIdO]):What haopens it No rate limits are ser? ((Detault benavior)Looking at current code:php// ProviderRateLimiter:: canMakeRequestforeach (Sprovider->getRateLimitso as SrateLimit) <if ($this=>rateLimiter->tooManyAttempts(...)) return false;return true:• If no rate Limits. returns TRUE (allows request)/ ProviderRateLimiter::incrementRequestCountforeach (Sorovider-saetRateLimits") as Sratelimit)// - If no rate limits, does NOTHING (no counters incremented)// ProviderRateLimniter:.requestAvailableTnreturn Sprovider->getRateLimits()->isNotEmpty()? Sprovider->getRateLimits()->map(...)->max()It no rate Limits,returns bcurrent derault benavior.• V Request is allowed (no throttling)• V No Redis counters incrementedWrotruattor = 0 cosonddThis means: If vou don't manuallv seed rate limits rows for a HubSoot Contiauration, reauests ao through unthrottled.This is probably NOT what you want for production. You should:1. Create a seeder to add default rate limits for all existing HubSpot configurations2. Add to Contiquration creation flow to auto-create defaults3. Or implement hardcoded fallbacks in code (Option 3 above)Mv PecommendationImmediate implementation (for testing)1. Use Option 3 (hybrid) - hardcoded defaults with DB override capabilit.2. Modifv executeRequest to accent <requestTvne narameter# hubspot webhook metricsapp/Jobs/Middleware/ D HandleRateLimit.php +42ann/lohelCrm/Doloto/ DolotoCrmEntitvTrait nhn t18select * from opportunities where team id = 2 and crm_provider_ id IN ('374720564' '14527423589' '49908861993|SELECT * FROM opportunity contacts WHERE opportunity_ id = '414'.SELECT * FROM opportunity contacts WHERE crm_provider id = '131501'•select * from contacts where id in (414)s/M RateLimitExcention.ohnAsk anvthing (&+bselect * From activitles where crm confiauration 1d = 2:<> CodeC AdantiveReiect alllW Windsurf Toams 716-1UTF.8IAccent allfo 4 spaces...
|
2693
|
NULL
|
NULL
|
NULL
|
|
2698
|
112
|
21
|
2026-05-07T11:36:05.474428+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153765474_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavigarecodeLaravelFV faVsco.js°9 mas PhostormVIewINavigarecodeLaravelFV faVsco.js°9 master ~Proledey(C) TranscodeParameterResolc) UserService.onp© Uuid.php> D Traits> D UseCases> O User> O Utils> C Validationphp nelpers.onpInitialFrontendState.phpc) Jiminny.phpc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial> M contrib• databaseO docsfront-endD langnode_modules library rootphpstanM oublic• M recaurcocy Mroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aol.onppnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage→aoo>M debuabarM frameworkv Mloas.aitianore• audio wav= custom.loaCo kematchactviyoncrmoojectbetach.ong© RateLimitException.php© SyncToUserPilot.php© Middleware/RateLimited.php© SyncTeamMetadata.php(C) ProviderRateLimiter.phpclass Cuient extends BasecLient imolements Hubspotcuientinterface6826836966971698713716719721=hubsnot-iournal-noll.log= laravel logus tht is* drexum arraysermrielduption>public function fetchOpportunityFieldOptions(Field $field): array{...}* othrows HubspotExceptionpublic function makeRequest(string Sendpoint. Smethod = 'GET'. Spavload = []. Pstring SauerystnSendooint = self::BASE URL sendooint:if Smethod =e=Sresnonse = Sthis-›aetinstanceo->oetcliento2->requestdendnoint: Sendoo.nt.query_string: $queryString3else {iSresponse = $this->getInstance)->getClient->request($method, $endpoint, ['json' => ($payload)Smax= Sresponse->getHeaderLine('X-HubSpot-RateLimit-Max'):sremarnino = sresponse-›cecheaderLlne x-huospoc-kaceLimic-kemaznino1/ "110"I/ "109"Sinterval= Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "100sbody= 1son decode((string) Sresponse->qetBodyoassociative: true)ILLuminate Support Facades Loq::channel channelcustom_channel'->1nfo'Smax"ILLuminate Support Facades Loq::channel channe'custom channel')->1nfo('Srema1ninaUuuLumanate Suobort Facades Loa: :channet channelcustom channel")»>info(SintervalluuuLumanate Suobort Facades Loa: :channet channe'custom channel')->info('$bod›* athrows RadReauest734 GT )nublin funcf: Lof &editsmde+ v Accept File 3~X.Rejest Fjle A48% 4 2 of 4 files →arQuhe for INE cuadesare ll Don't ack adain (todav 10-25)Support Daily - in 24 m100% 12Thu 7 May 14:36:05CleanShot is ready to use!= larapel.log4 SF jiminny@localhost]A HS_local (jiminny@localhost]« console [PROD]« console [EU]cascadeA console [STAGING]Hubspot Rate Limitind+0 ..lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ npx screenpipe@latest reco m V15 ~sth1s->makeremobucketda1ly'. 250000, 864661.Start recording screen, audio, and serve the APIUsage: screenpipe record [OPTIONS]Uptions:What haooens it No rate limits are ser? ((Detault benavior)-d. --aud10-chunk-duration <AUDIO CHUNK DURATION>Looking at current code:Aud1o chunk duration in secondsphpdefault: 30// ProviderRateLimiter:: canMakeRequestforeach (Sprovider->getRateLimitso as SrateLimit) <-p. --port <PORT>if ($this=>rateLimiter->tooManyAttempts(...)) return false;Port to run the server orreturn true:/* If no rate Limits. returns TRUE (allows request)default: 30301/ ProviderRateLimiter::incrementRequestCountforeach (Sorovider-saetRateLimits") as Sratelimit)--disahi e-audinDisable audio recording/1 * It no rate Limits, does NOTHING (no counters incremented)i, --audio-device <AUDIO_DEVICE>Audio devices to use (can be specified multiple times)// ProviderRateLimniter:.requestAvailableTnreturn Sprovider->getRateLimits()->isNotEmpty()? Sprovider->getRateLimits()->map(...)->max()It no rate Limits,returns bFollow system default audio devicescurrent derault benavior.--experzmental-coreavolo-systen-aualo• V Request is allowed (no throttling)[experimental, macOS 14.4+] Capture System Audio via CoreAudio Process Tap instead of ScreenCaptur• V No Redis counters incremented--data-dir <DATA DIR>•• retryAfter = 0 secondsData directory. Default to $HOME/.screenpipeThis means: If vou don't manuallv seed rate limits rows for a HubSoot Contiauration, reauests ao through unthrottled.This is probably NOT what you want for production. You should:1. Create a seeder to add default rate limits for all existing HubSpot configurationsEnable debua loggina for screenpipe modules2. Add to Contiquration creation flow to auto-create defaults3. Or implement hardcoded fallbacks in code (Option 3 above)-a. --audio-transcrintion-enqine <AUDIO TRANSCRIPTION ENGINESAudio transcrintion enqine to useMv PecommendationImmediate implementation (for testing)Possible values:- deepgram- whisnen-tinv1. Use Option 3 (hybrid) - hardcoded defaults with DB override capabilit.2. Modifv executeRequest to accent <reauestTvne narameter- whisnen-tinv-quantized- whisper-largewhisnen-lanae-quantizedapp/Jobs/Middleware/ D HandleRateLimit.php +42ann/lohelCrm/Doloto/ DolotoCrmEntitvTrait nhn t18- whisnen-larae-v3-tunho- whicnon-lanno-vZ-tunho-quantizods/M RateLimitExcention.ohnalaoawealmiawtalReiect alllAccent alli- awonZ-aen- nanakootAsk anvthing (&+b_ dicablod.Disable transcription (audio capture only., no speech-to-text)<> CodeC° AdantiveW Windsurf Toamc...
|
NULL
|
990208574192800611
|
NULL
|
visual_change
|
ocr
|
NULL
|
PhostormVIewINavigarecodeLaravelFV faVsco.js°9 mas PhostormVIewINavigarecodeLaravelFV faVsco.js°9 master ~Proledey(C) TranscodeParameterResolc) UserService.onp© Uuid.php> D Traits> D UseCases> O User> O Utils> C Validationphp nelpers.onpInitialFrontendState.phpc) Jiminny.phpc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial> M contrib• databaseO docsfront-endD langnode_modules library rootphpstanM oublic• M recaurcocy Mroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aol.onppnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage→aoo>M debuabarM frameworkv Mloas.aitianore• audio wav= custom.loaCo kematchactviyoncrmoojectbetach.ong© RateLimitException.php© SyncToUserPilot.php© Middleware/RateLimited.php© SyncTeamMetadata.php(C) ProviderRateLimiter.phpclass Cuient extends BasecLient imolements Hubspotcuientinterface6826836966971698713716719721=hubsnot-iournal-noll.log= laravel logus tht is* drexum arraysermrielduption>public function fetchOpportunityFieldOptions(Field $field): array{...}* othrows HubspotExceptionpublic function makeRequest(string Sendpoint. Smethod = 'GET'. Spavload = []. Pstring SauerystnSendooint = self::BASE URL sendooint:if Smethod =e=Sresnonse = Sthis-›aetinstanceo->oetcliento2->requestdendnoint: Sendoo.nt.query_string: $queryString3else {iSresponse = $this->getInstance)->getClient->request($method, $endpoint, ['json' => ($payload)Smax= Sresponse->getHeaderLine('X-HubSpot-RateLimit-Max'):sremarnino = sresponse-›cecheaderLlne x-huospoc-kaceLimic-kemaznino1/ "110"I/ "109"Sinterval= Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "100sbody= 1son decode((string) Sresponse->qetBodyoassociative: true)ILLuminate Support Facades Loq::channel channelcustom_channel'->1nfo'Smax"ILLuminate Support Facades Loq::channel channe'custom channel')->1nfo('Srema1ninaUuuLumanate Suobort Facades Loa: :channet channelcustom channel")»>info(SintervalluuuLumanate Suobort Facades Loa: :channet channe'custom channel')->info('$bod›* athrows RadReauest734 GT )nublin funcf: Lof &editsmde+ v Accept File 3~X.Rejest Fjle A48% 4 2 of 4 files →arQuhe for INE cuadesare ll Don't ack adain (todav 10-25)Support Daily - in 24 m100% 12Thu 7 May 14:36:05CleanShot is ready to use!= larapel.log4 SF jiminny@localhost]A HS_local (jiminny@localhost]« console [PROD]« console [EU]cascadeA console [STAGING]Hubspot Rate Limitind+0 ..lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ npx screenpipe@latest reco m V15 ~sth1s->makeremobucketda1ly'. 250000, 864661.Start recording screen, audio, and serve the APIUsage: screenpipe record [OPTIONS]Uptions:What haooens it No rate limits are ser? ((Detault benavior)-d. --aud10-chunk-duration <AUDIO CHUNK DURATION>Looking at current code:Aud1o chunk duration in secondsphpdefault: 30// ProviderRateLimiter:: canMakeRequestforeach (Sprovider->getRateLimitso as SrateLimit) <-p. --port <PORT>if ($this=>rateLimiter->tooManyAttempts(...)) return false;Port to run the server orreturn true:/* If no rate Limits. returns TRUE (allows request)default: 30301/ ProviderRateLimiter::incrementRequestCountforeach (Sorovider-saetRateLimits") as Sratelimit)--disahi e-audinDisable audio recording/1 * It no rate Limits, does NOTHING (no counters incremented)i, --audio-device <AUDIO_DEVICE>Audio devices to use (can be specified multiple times)// ProviderRateLimniter:.requestAvailableTnreturn Sprovider->getRateLimits()->isNotEmpty()? Sprovider->getRateLimits()->map(...)->max()It no rate Limits,returns bFollow system default audio devicescurrent derault benavior.--experzmental-coreavolo-systen-aualo• V Request is allowed (no throttling)[experimental, macOS 14.4+] Capture System Audio via CoreAudio Process Tap instead of ScreenCaptur• V No Redis counters incremented--data-dir <DATA DIR>•• retryAfter = 0 secondsData directory. Default to $HOME/.screenpipeThis means: If vou don't manuallv seed rate limits rows for a HubSoot Contiauration, reauests ao through unthrottled.This is probably NOT what you want for production. You should:1. Create a seeder to add default rate limits for all existing HubSpot configurationsEnable debua loggina for screenpipe modules2. Add to Contiquration creation flow to auto-create defaults3. Or implement hardcoded fallbacks in code (Option 3 above)-a. --audio-transcrintion-enqine <AUDIO TRANSCRIPTION ENGINESAudio transcrintion enqine to useMv PecommendationImmediate implementation (for testing)Possible values:- deepgram- whisnen-tinv1. Use Option 3 (hybrid) - hardcoded defaults with DB override capabilit.2. Modifv executeRequest to accent <reauestTvne narameter- whisnen-tinv-quantized- whisper-largewhisnen-lanae-quantizedapp/Jobs/Middleware/ D HandleRateLimit.php +42ann/lohelCrm/Doloto/ DolotoCrmEntitvTrait nhn t18- whisnen-larae-v3-tunho- whicnon-lanno-vZ-tunho-quantizods/M RateLimitExcention.ohnalaoawealmiawtalReiect alllAccent alli- awonZ-aen- nanakootAsk anvthing (&+b_ dicablod.Disable transcription (audio capture only., no speech-to-text)<> CodeC° AdantiveW Windsurf Toamc...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2700
|
112
|
22
|
2026-05-07T11:36:09.415867+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153769415_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {"host":"docker_lamp_1"} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {"host":"docker_lamp_1","events":2} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {"isMonday":true,"isWeekend":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {"offset":"","jiminny_team_id":1} {"correlation_id":"b453f0f4-0d2c-491c-a935-14c60265ec18","trace_id":"a50202d6-5601-46b4-a02e-cf5ac8d061fa"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {"url":"[URL_WITH_CREDENTIALS] {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {"inbox_id":212} {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:27:04] 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":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring start {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring end {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:08] 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":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:08] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:14] local.INFO: [Jiminny\Jobs\Mailbox\CreateBatches] processed 2 inboxes and created 1 batches {"userId":null,"batchSize":30,"maxBatches":1000} {"correlation_id":"98416296-43c2-4834-927e-4a0e86dc99cf","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:28:04] 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":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"dfad8817-1dde-4622-8320-f5fc1616a8fe","trace_id":"d21232b2-60fb-4ca0-838c-ba42e61593f9"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring start {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring end {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"de768781-5c05-418c-baf3-79f77a8c8694","trace_id":"fec74062-3f6d-4cfc-968a-1bb285496db3"}
[2026-04-27 10:28:12] 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":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framewo...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {\"isMonday\":true,\"isWeekend\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":273.7,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [Send Report Not Generated Mail] Email sent {\"uuid\":\"cd5b5081-22f8-4d1a-9d6f-f82aca1121f6\",\"email\":\"lukas.kovalik@jiminny.com\"} {\"correlation_id\":\"4255983f-07a4-4872-9ea6-5491fa32bdd5\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Inbox service] Skipping METADATA SYNC for inbox 59 due to unauthorized access to the mailbox {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] Performing incremental sync for inbox 212 using history ID: @1777284288 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:27:04] 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\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring start {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring end {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:08] 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\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:14] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 1 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"98416296-43c2-4834-927e-4a0e86dc99cf\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:28:04] 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\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"dfad8817-1dde-4622-8320-f5fc1616a8fe\",\"trace_id\":\"d21232b2-60fb-4ca0-838c-ba42e61593f9\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"de768781-5c05-418c-baf3-79f77a8c8694\",\"trace_id\":\"fec74062-3f6d-4cfc-968a-1bb285496db3\"}\n[2026-04-27 10:28:12] 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\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"d8709bb9-0f8c-4798-a8c7-e68e24ab6265\",\"trace_id\":\"c5bea1cc-53fe-4bda-a8e2-bec002030bc2\"}\n[2026-04-27 10:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: Processing email batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce788ac964bbf\",\"from\":\"Nikolay Yankov <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"message_id\":\"<jiminny/app/pull/11998/before/2b8a97d6be8b053441d5a845b72dd04803cbb53c/after/8a68a94d39cc76ae546e19f354e8b0dabd577314@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce7883f2fea19\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"message_id\":\"<d70689e8-7b14-42ac-a3bf-5f1e81da943e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce761cb049fba\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"message_id\":\"<017f0e66-9989-446e-9788-7ae83a99137e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce74ed398d2c1\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Lukas Kovalik <kovaliklukas@gmail.com>, Author <author@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"message_id\":\"<jiminny/app/pull/12016/c4326142661@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce713dbecf41f\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"message_id\":\"<jiminny/infrastructure/pull/725/c4326108250@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce708ed8b5acb\",\"from\":\"Veselin Kulov <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"message_id\":\"<jiminny/infrastructure/pull/725/before/186952cc584428fcca17aa364f6cc535d65e804a/after/078246a06b25fb93ad61c7f14472ecb755483f4d@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce6eea657f2fa\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"message_id\":\"<jiminny/app/pull/11998/c4326088534@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Deleting successfully processed batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:17] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"46b082fc-5a28-4cc4-8bd1-c670f8662e7f\",\"trace_id\":\"8ae7dc26-61c7-42e4-8617-0b85f53e54c1\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] 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\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.NOTICE: Calendar sync start {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 2be54cfa-1691-4388-a284-754fdf414400 Correlation ID: 9a934049-899b-45c5-99a5-15c9c470d8a2 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"2be54cfa-1691-4388-a284-754fdf414400\\\",\\\"correlation_id\\\":\\\"9a934049-899b-45c5-99a5-15c9c470d8a2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 27729e9a-0943-41bd-9752-4ec9a8444500 Correlation ID: 824a9761-9387-4c53-ae31-5c242b1d4298 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"27729e9a-0943-41bd-9752-4ec9a8444500\\\",\\\"correlation_id\\\":\\\"824a9761-9387-4c53-ae31-5c242b1d4298\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1502,\"provider\":\"google\",\"refreshToken\":\"d417c92ebaa137295a04675f715d0511ae8acac9d779b102eac50d6300116d3e\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1502,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] 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: 73b326f0-9fcd-4ff5-ad61-edd6a0504500 Correlation ID: 6bd8fef3-b24c-46d8-8bba-a9b2ab41c108 Timestamp: 2026-04-27 10:28:28Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:28Z\\\",\\\"trace_id\\\":\\\"73b326f0-9fcd-4ff5-ad61-edd6a0504500\\\",\\\"correlation_id\\\":\\\"6bd8fef3-b24c-46d8-8bba-a9b2ab41c108\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: c5f1db6f-22fb-41cc-bd89-c46497634200 Correlation ID: 7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"c5f1db6f-22fb-41cc-bd89-c46497634200\\\",\\\"correlation_id\\\":\\\"7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: 8d6e82ba-88c3-4ec9-95ee-a4a905803900 Correlation ID: 6ede51d5-704d-4e24-9fb7-88f69f3da5db Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"8d6e82ba-88c3-4ec9-95ee-a4a905803900\\\",\\\"correlation_id\\\":\\\"6ede51d5-704d-4e24-9fb7-88f69f3da5db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1115,\"provider\":\"google\",\"refreshToken\":\"356b60f12e262a5e24d3042386ef47d6a6cfe3074c242f4426edcec8646192b1\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1115,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1421,\"provider\":\"office\",\"refreshToken\":\"9e3e287aa0cc900726e3d4eead2600fb64aa34b437790f6a1fd8048306cb2db9\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Refresh token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1421,\"provider\":\"office\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] 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\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCMx0nGVsTQEodM41jSlu-5eY2JUTYHZXE-bDro2tnepp8q_Vq3ITNS8CkncjGjcBfaHkza05dCTIlaXcSIoPNGOl9vNckYy_ZhVgE1SWvgRcCeVT-SyogdB9hP-oaWbH9r51OQrrzAwmalK5aCksyUVh9jG_A-mGVzRPnUB4SKa77o5P659lpx1egON48YU2tQ.D2oErKma-mQrBdXxAKdH7beUkp6HS2PXUWYLCgvbP2c\",\"last_sync\":\"2026-04-27 06:28:41\",\"dateMode\":\"daily\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:29:06] 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\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring start {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring end {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:12] 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\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:30:08] 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\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30: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\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring start {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring end {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:21] 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\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Running conference:monitor:start command for activities in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: [conference:monitor:start] No activities found in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:43] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:43] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"10:25\",\"to\":\"10:30\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"00:20\",\"to\":\"00:25\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:51] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-04-27T10:32:57.492763Z\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:58] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811739,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811740,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811741,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811742,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811743,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811744,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SyncActivity] Start {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.ALERT: [SyncActivity] Failed {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] End {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Memory usage {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25149472,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] End {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Memory usage {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25527752,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQC41CLa_xP1bAbi9E2OUbAOpW..Wx961OtqTwJ3oWpOKs6yPdm47B4yKRBHbRmmJ.Lvd34sjr0gnAkV8fLb8IZWzIW93\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ALERT: [SyncActivity] Failed {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] End {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] Memory usage {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25679680,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":408,\"provider\":\"hubspot\",\"refreshToken\":\"de4e47eb985578f4218833e763e31059e88b562e87e10749b3389be2328f0aa7\",\"state\":\"connected\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:12] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}","depth":4,"bounds":{"left":0.52859044,"top":0.0726257,"width":0.45977393,"height":0.9273743},"on_screen":true,"value":"[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {\"isMonday\":true,\"isWeekend\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":273.7,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [Send Report Not Generated Mail] Email sent {\"uuid\":\"cd5b5081-22f8-4d1a-9d6f-f82aca1121f6\",\"email\":\"lukas.kovalik@jiminny.com\"} {\"correlation_id\":\"4255983f-07a4-4872-9ea6-5491fa32bdd5\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Inbox service] Skipping METADATA SYNC for inbox 59 due to unauthorized access to the mailbox {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] Performing incremental sync for inbox 212 using history ID: @1777284288 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:27:04] 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\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring start {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring end {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:08] 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\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:14] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 1 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"98416296-43c2-4834-927e-4a0e86dc99cf\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:28:04] 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\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"dfad8817-1dde-4622-8320-f5fc1616a8fe\",\"trace_id\":\"d21232b2-60fb-4ca0-838c-ba42e61593f9\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"de768781-5c05-418c-baf3-79f77a8c8694\",\"trace_id\":\"fec74062-3f6d-4cfc-968a-1bb285496db3\"}\n[2026-04-27 10:28:12] 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\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"d8709bb9-0f8c-4798-a8c7-e68e24ab6265\",\"trace_id\":\"c5bea1cc-53fe-4bda-a8e2-bec002030bc2\"}\n[2026-04-27 10:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: Processing email batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce788ac964bbf\",\"from\":\"Nikolay Yankov <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"message_id\":\"<jiminny/app/pull/11998/before/2b8a97d6be8b053441d5a845b72dd04803cbb53c/after/8a68a94d39cc76ae546e19f354e8b0dabd577314@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce7883f2fea19\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"message_id\":\"<d70689e8-7b14-42ac-a3bf-5f1e81da943e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce761cb049fba\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"message_id\":\"<017f0e66-9989-446e-9788-7ae83a99137e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce74ed398d2c1\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Lukas Kovalik <kovaliklukas@gmail.com>, Author <author@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"message_id\":\"<jiminny/app/pull/12016/c4326142661@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce713dbecf41f\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"message_id\":\"<jiminny/infrastructure/pull/725/c4326108250@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce708ed8b5acb\",\"from\":\"Veselin Kulov <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"message_id\":\"<jiminny/infrastructure/pull/725/before/186952cc584428fcca17aa364f6cc535d65e804a/after/078246a06b25fb93ad61c7f14472ecb755483f4d@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce6eea657f2fa\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"message_id\":\"<jiminny/app/pull/11998/c4326088534@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Deleting successfully processed batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:17] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"46b082fc-5a28-4cc4-8bd1-c670f8662e7f\",\"trace_id\":\"8ae7dc26-61c7-42e4-8617-0b85f53e54c1\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] 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\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.NOTICE: Calendar sync start {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 2be54cfa-1691-4388-a284-754fdf414400 Correlation ID: 9a934049-899b-45c5-99a5-15c9c470d8a2 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"2be54cfa-1691-4388-a284-754fdf414400\\\",\\\"correlation_id\\\":\\\"9a934049-899b-45c5-99a5-15c9c470d8a2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 27729e9a-0943-41bd-9752-4ec9a8444500 Correlation ID: 824a9761-9387-4c53-ae31-5c242b1d4298 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"27729e9a-0943-41bd-9752-4ec9a8444500\\\",\\\"correlation_id\\\":\\\"824a9761-9387-4c53-ae31-5c242b1d4298\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1502,\"provider\":\"google\",\"refreshToken\":\"d417c92ebaa137295a04675f715d0511ae8acac9d779b102eac50d6300116d3e\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1502,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] 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: 73b326f0-9fcd-4ff5-ad61-edd6a0504500 Correlation ID: 6bd8fef3-b24c-46d8-8bba-a9b2ab41c108 Timestamp: 2026-04-27 10:28:28Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:28Z\\\",\\\"trace_id\\\":\\\"73b326f0-9fcd-4ff5-ad61-edd6a0504500\\\",\\\"correlation_id\\\":\\\"6bd8fef3-b24c-46d8-8bba-a9b2ab41c108\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: c5f1db6f-22fb-41cc-bd89-c46497634200 Correlation ID: 7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"c5f1db6f-22fb-41cc-bd89-c46497634200\\\",\\\"correlation_id\\\":\\\"7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: 8d6e82ba-88c3-4ec9-95ee-a4a905803900 Correlation ID: 6ede51d5-704d-4e24-9fb7-88f69f3da5db Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"8d6e82ba-88c3-4ec9-95ee-a4a905803900\\\",\\\"correlation_id\\\":\\\"6ede51d5-704d-4e24-9fb7-88f69f3da5db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1115,\"provider\":\"google\",\"refreshToken\":\"356b60f12e262a5e24d3042386ef47d6a6cfe3074c242f4426edcec8646192b1\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1115,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1421,\"provider\":\"office\",\"refreshToken\":\"9e3e287aa0cc900726e3d4eead2600fb64aa34b437790f6a1fd8048306cb2db9\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Refresh token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1421,\"provider\":\"office\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] 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\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCMx0nGVsTQEodM41jSlu-5eY2JUTYHZXE-bDro2tnepp8q_Vq3ITNS8CkncjGjcBfaHkza05dCTIlaXcSIoPNGOl9vNckYy_ZhVgE1SWvgRcCeVT-SyogdB9hP-oaWbH9r51OQrrzAwmalK5aCksyUVh9jG_A-mGVzRPnUB4SKa77o5P659lpx1egON48YU2tQ.D2oErKma-mQrBdXxAKdH7beUkp6HS2PXUWYLCgvbP2c\",\"last_sync\":\"2026-04-27 06:28:41\",\"dateMode\":\"daily\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:29:06] 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\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring start {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring end {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:12] 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\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:30:08] 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\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30: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\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring start {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring end {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:21] 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\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Running conference:monitor:start command for activities in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: [conference:monitor:start] No activities found in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:43] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:43] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"10:25\",\"to\":\"10:30\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"00:20\",\"to\":\"00:25\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:51] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-04-27T10:32:57.492763Z\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:58] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811739,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811740,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811741,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811742,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811743,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811744,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SyncActivity] Start {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.ALERT: [SyncActivity] Failed {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] End {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Memory usage {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25149472,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] End {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Memory usage {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25527752,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQC41CLa_xP1bAbi9E2OUbAOpW..Wx961OtqTwJ3oWpOKs6yPdm47B4yKRBHbRmmJ.Lvd34sjr0gnAkV8fLb8IZWzIW93\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ALERT: [SyncActivity] Failed {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] End {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] Memory usage {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25679680,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":408,\"provider\":\"hubspot\",\"refreshToken\":\"de4e47eb985578f4218833e763e31059e88b562e87e10749b3389be2328f0aa7\",\"state\":\"connected\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:12] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Service, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BaseRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EfficientJsonParser.php, class","depth":11,"on_screen":false,"role_description":"text"}]...
|
-6881570513140039651
|
-8151256403349000395
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {"host":"docker_lamp_1"} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {"host":"docker_lamp_1","events":2} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {"isMonday":true,"isWeekend":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {"offset":"","jiminny_team_id":1} {"correlation_id":"b453f0f4-0d2c-491c-a935-14c60265ec18","trace_id":"a50202d6-5601-46b4-a02e-cf5ac8d061fa"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {"url":"[URL_WITH_CREDENTIALS] {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {"inbox_id":212} {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:27:04] 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":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring start {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring end {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:08] 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":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:08] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:14] local.INFO: [Jiminny\Jobs\Mailbox\CreateBatches] processed 2 inboxes and created 1 batches {"userId":null,"batchSize":30,"maxBatches":1000} {"correlation_id":"98416296-43c2-4834-927e-4a0e86dc99cf","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:28:04] 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":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"dfad8817-1dde-4622-8320-f5fc1616a8fe","trace_id":"d21232b2-60fb-4ca0-838c-ba42e61593f9"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring start {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring end {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"de768781-5c05-418c-baf3-79f77a8c8694","trace_id":"fec74062-3f6d-4cfc-968a-1bb285496db3"}
[2026-04-27 10:28:12] 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":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framewo...
|
2698
|
NULL
|
NULL
|
NULL
|
|
2701
|
112
|
23
|
2026-05-07T11:36:12.309254+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153772309_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
This view is read-only
text/html
text/html
text/ht This view is read-only
text/html
text/html
text/html
Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {"host":"docker_lamp_1"} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {"host":"docker_lamp_1","events":2} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {"isMonday":true,"isWeekend":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {"offset":"","jiminny_team_id":1} {"correlation_id":"b453f0f4-0d2c-491c-a935-14c60265ec18","trace_id":"a50202d6-5601-46b4-a02e-cf5ac8d061fa"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {"url":"[URL_WITH_CREDENTIALS] {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {"inbox_id":212} {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:27:04] 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":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring start {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring end {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:08] 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":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:08] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:14] local.INFO: [Jiminny\Jobs\Mailbox\CreateBatches] processed 2 inboxes and created 1 batches {"userId":null,"batchSize":30,"maxBatches":1000} {"correlation_id":"98416296-43c2-4834-927e-4a0e86dc99cf","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:28:04] 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":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"dfad8817-1dde-4622-8320-f5fc1616a8fe","trace_id":"d21232b2-60fb-4ca0-838c-ba42e61593f9"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring start {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring end {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"de768781-5c05-418c-baf3-79f77a8c8694","trace_id":"fec74062-3f6d-4cfc-968a-1bb285496db3"}
[2026-04-27 10:28:12] 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":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposi...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"This view is read-only","depth":2,"bounds":{"left":0.5794548,"top":0.16280925,"width":0.04488032,"height":0.013567438},"on_screen":true,"value":"This view is read-only","help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":3,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":3,"bounds":{"left":0.5794548,"top":0.16280925,"width":0.04488032,"height":0.013567438},"on_screen":true,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"text/html","depth":3,"on_screen":false,"help_text":"text/html","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {\"isMonday\":true,\"isWeekend\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":273.7,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [Send Report Not Generated Mail] Email sent {\"uuid\":\"cd5b5081-22f8-4d1a-9d6f-f82aca1121f6\",\"email\":\"lukas.kovalik@jiminny.com\"} {\"correlation_id\":\"4255983f-07a4-4872-9ea6-5491fa32bdd5\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Inbox service] Skipping METADATA SYNC for inbox 59 due to unauthorized access to the mailbox {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] Performing incremental sync for inbox 212 using history ID: @1777284288 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:27:04] 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\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring start {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring end {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:08] 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\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:14] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 1 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"98416296-43c2-4834-927e-4a0e86dc99cf\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:28:04] 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\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"dfad8817-1dde-4622-8320-f5fc1616a8fe\",\"trace_id\":\"d21232b2-60fb-4ca0-838c-ba42e61593f9\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"de768781-5c05-418c-baf3-79f77a8c8694\",\"trace_id\":\"fec74062-3f6d-4cfc-968a-1bb285496db3\"}\n[2026-04-27 10:28:12] 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\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"d8709bb9-0f8c-4798-a8c7-e68e24ab6265\",\"trace_id\":\"c5bea1cc-53fe-4bda-a8e2-bec002030bc2\"}\n[2026-04-27 10:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: Processing email batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce788ac964bbf\",\"from\":\"Nikolay Yankov <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"message_id\":\"<jiminny/app/pull/11998/before/2b8a97d6be8b053441d5a845b72dd04803cbb53c/after/8a68a94d39cc76ae546e19f354e8b0dabd577314@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce7883f2fea19\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"message_id\":\"<d70689e8-7b14-42ac-a3bf-5f1e81da943e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce761cb049fba\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"message_id\":\"<017f0e66-9989-446e-9788-7ae83a99137e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce74ed398d2c1\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Lukas Kovalik <kovaliklukas@gmail.com>, Author <author@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"message_id\":\"<jiminny/app/pull/12016/c4326142661@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce713dbecf41f\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"message_id\":\"<jiminny/infrastructure/pull/725/c4326108250@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce708ed8b5acb\",\"from\":\"Veselin Kulov <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"message_id\":\"<jiminny/infrastructure/pull/725/before/186952cc584428fcca17aa364f6cc535d65e804a/after/078246a06b25fb93ad61c7f14472ecb755483f4d@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce6eea657f2fa\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"message_id\":\"<jiminny/app/pull/11998/c4326088534@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Deleting successfully processed batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:17] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"46b082fc-5a28-4cc4-8bd1-c670f8662e7f\",\"trace_id\":\"8ae7dc26-61c7-42e4-8617-0b85f53e54c1\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] 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\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.NOTICE: Calendar sync start {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 2be54cfa-1691-4388-a284-754fdf414400 Correlation ID: 9a934049-899b-45c5-99a5-15c9c470d8a2 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"2be54cfa-1691-4388-a284-754fdf414400\\\",\\\"correlation_id\\\":\\\"9a934049-899b-45c5-99a5-15c9c470d8a2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 27729e9a-0943-41bd-9752-4ec9a8444500 Correlation ID: 824a9761-9387-4c53-ae31-5c242b1d4298 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"27729e9a-0943-41bd-9752-4ec9a8444500\\\",\\\"correlation_id\\\":\\\"824a9761-9387-4c53-ae31-5c242b1d4298\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1502,\"provider\":\"google\",\"refreshToken\":\"d417c92ebaa137295a04675f715d0511ae8acac9d779b102eac50d6300116d3e\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1502,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] 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: 73b326f0-9fcd-4ff5-ad61-edd6a0504500 Correlation ID: 6bd8fef3-b24c-46d8-8bba-a9b2ab41c108 Timestamp: 2026-04-27 10:28:28Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:28Z\\\",\\\"trace_id\\\":\\\"73b326f0-9fcd-4ff5-ad61-edd6a0504500\\\",\\\"correlation_id\\\":\\\"6bd8fef3-b24c-46d8-8bba-a9b2ab41c108\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: c5f1db6f-22fb-41cc-bd89-c46497634200 Correlation ID: 7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"c5f1db6f-22fb-41cc-bd89-c46497634200\\\",\\\"correlation_id\\\":\\\"7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: 8d6e82ba-88c3-4ec9-95ee-a4a905803900 Correlation ID: 6ede51d5-704d-4e24-9fb7-88f69f3da5db Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"8d6e82ba-88c3-4ec9-95ee-a4a905803900\\\",\\\"correlation_id\\\":\\\"6ede51d5-704d-4e24-9fb7-88f69f3da5db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1115,\"provider\":\"google\",\"refreshToken\":\"356b60f12e262a5e24d3042386ef47d6a6cfe3074c242f4426edcec8646192b1\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1115,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1421,\"provider\":\"office\",\"refreshToken\":\"9e3e287aa0cc900726e3d4eead2600fb64aa34b437790f6a1fd8048306cb2db9\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Refresh token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1421,\"provider\":\"office\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] 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\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCMx0nGVsTQEodM41jSlu-5eY2JUTYHZXE-bDro2tnepp8q_Vq3ITNS8CkncjGjcBfaHkza05dCTIlaXcSIoPNGOl9vNckYy_ZhVgE1SWvgRcCeVT-SyogdB9hP-oaWbH9r51OQrrzAwmalK5aCksyUVh9jG_A-mGVzRPnUB4SKa77o5P659lpx1egON48YU2tQ.D2oErKma-mQrBdXxAKdH7beUkp6HS2PXUWYLCgvbP2c\",\"last_sync\":\"2026-04-27 06:28:41\",\"dateMode\":\"daily\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:29:06] 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\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring start {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring end {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:12] 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\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:30:08] 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\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30: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\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring start {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring end {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:21] 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\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Running conference:monitor:start command for activities in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: [conference:monitor:start] No activities found in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:43] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:43] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"10:25\",\"to\":\"10:30\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"00:20\",\"to\":\"00:25\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:51] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-04-27T10:32:57.492763Z\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:58] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811739,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811740,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811741,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811742,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811743,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811744,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SyncActivity] Start {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.ALERT: [SyncActivity] Failed {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] End {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Memory usage {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25149472,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] End {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Memory usage {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25527752,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQC41CLa_xP1bAbi9E2OUbAOpW..Wx961OtqTwJ3oWpOKs6yPdm47B4yKRBHbRmmJ.Lvd34sjr0gnAkV8fLb8IZWzIW93\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ALERT: [SyncActivity] Failed {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] End {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] Memory usage {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25679680,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":408,\"provider\":\"hubspot\",\"refreshToken\":\"de4e47eb985578f4218833e763e31059e88b562e87e10749b3389be2328f0aa7\",\"state\":\"connected\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:12] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}","depth":4,"bounds":{"left":0.52859044,"top":0.0726257,"width":0.45977393,"height":0.9273743},"on_screen":true,"value":"[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {\"isMonday\":true,\"isWeekend\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":273.7,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [Send Report Not Generated Mail] Email sent {\"uuid\":\"cd5b5081-22f8-4d1a-9d6f-f82aca1121f6\",\"email\":\"lukas.kovalik@jiminny.com\"} {\"correlation_id\":\"4255983f-07a4-4872-9ea6-5491fa32bdd5\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Inbox service] Skipping METADATA SYNC for inbox 59 due to unauthorized access to the mailbox {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] Performing incremental sync for inbox 212 using history ID: @1777284288 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:27:04] 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\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring start {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring end {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:08] 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\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:14] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 1 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"98416296-43c2-4834-927e-4a0e86dc99cf\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:28:04] 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\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"dfad8817-1dde-4622-8320-f5fc1616a8fe\",\"trace_id\":\"d21232b2-60fb-4ca0-838c-ba42e61593f9\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"de768781-5c05-418c-baf3-79f77a8c8694\",\"trace_id\":\"fec74062-3f6d-4cfc-968a-1bb285496db3\"}\n[2026-04-27 10:28:12] 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\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"d8709bb9-0f8c-4798-a8c7-e68e24ab6265\",\"trace_id\":\"c5bea1cc-53fe-4bda-a8e2-bec002030bc2\"}\n[2026-04-27 10:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: Processing email batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce788ac964bbf\",\"from\":\"Nikolay Yankov <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"message_id\":\"<jiminny/app/pull/11998/before/2b8a97d6be8b053441d5a845b72dd04803cbb53c/after/8a68a94d39cc76ae546e19f354e8b0dabd577314@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce7883f2fea19\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"message_id\":\"<d70689e8-7b14-42ac-a3bf-5f1e81da943e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce761cb049fba\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"message_id\":\"<017f0e66-9989-446e-9788-7ae83a99137e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce74ed398d2c1\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Lukas Kovalik <kovaliklukas@gmail.com>, Author <author@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"message_id\":\"<jiminny/app/pull/12016/c4326142661@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce713dbecf41f\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"message_id\":\"<jiminny/infrastructure/pull/725/c4326108250@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce708ed8b5acb\",\"from\":\"Veselin Kulov <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"message_id\":\"<jiminny/infrastructure/pull/725/before/186952cc584428fcca17aa364f6cc535d65e804a/after/078246a06b25fb93ad61c7f14472ecb755483f4d@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce6eea657f2fa\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"message_id\":\"<jiminny/app/pull/11998/c4326088534@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Deleting successfully processed batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:17] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"46b082fc-5a28-4cc4-8bd1-c670f8662e7f\",\"trace_id\":\"8ae7dc26-61c7-42e4-8617-0b85f53e54c1\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] 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\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.NOTICE: Calendar sync start {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 2be54cfa-1691-4388-a284-754fdf414400 Correlation ID: 9a934049-899b-45c5-99a5-15c9c470d8a2 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"2be54cfa-1691-4388-a284-754fdf414400\\\",\\\"correlation_id\\\":\\\"9a934049-899b-45c5-99a5-15c9c470d8a2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 27729e9a-0943-41bd-9752-4ec9a8444500 Correlation ID: 824a9761-9387-4c53-ae31-5c242b1d4298 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"27729e9a-0943-41bd-9752-4ec9a8444500\\\",\\\"correlation_id\\\":\\\"824a9761-9387-4c53-ae31-5c242b1d4298\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1502,\"provider\":\"google\",\"refreshToken\":\"d417c92ebaa137295a04675f715d0511ae8acac9d779b102eac50d6300116d3e\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1502,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] 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: 73b326f0-9fcd-4ff5-ad61-edd6a0504500 Correlation ID: 6bd8fef3-b24c-46d8-8bba-a9b2ab41c108 Timestamp: 2026-04-27 10:28:28Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:28Z\\\",\\\"trace_id\\\":\\\"73b326f0-9fcd-4ff5-ad61-edd6a0504500\\\",\\\"correlation_id\\\":\\\"6bd8fef3-b24c-46d8-8bba-a9b2ab41c108\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: c5f1db6f-22fb-41cc-bd89-c46497634200 Correlation ID: 7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"c5f1db6f-22fb-41cc-bd89-c46497634200\\\",\\\"correlation_id\\\":\\\"7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: 8d6e82ba-88c3-4ec9-95ee-a4a905803900 Correlation ID: 6ede51d5-704d-4e24-9fb7-88f69f3da5db Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"8d6e82ba-88c3-4ec9-95ee-a4a905803900\\\",\\\"correlation_id\\\":\\\"6ede51d5-704d-4e24-9fb7-88f69f3da5db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1115,\"provider\":\"google\",\"refreshToken\":\"356b60f12e262a5e24d3042386ef47d6a6cfe3074c242f4426edcec8646192b1\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1115,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1421,\"provider\":\"office\",\"refreshToken\":\"9e3e287aa0cc900726e3d4eead2600fb64aa34b437790f6a1fd8048306cb2db9\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Refresh token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1421,\"provider\":\"office\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] 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\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCMx0nGVsTQEodM41jSlu-5eY2JUTYHZXE-bDro2tnepp8q_Vq3ITNS8CkncjGjcBfaHkza05dCTIlaXcSIoPNGOl9vNckYy_ZhVgE1SWvgRcCeVT-SyogdB9hP-oaWbH9r51OQrrzAwmalK5aCksyUVh9jG_A-mGVzRPnUB4SKa77o5P659lpx1egON48YU2tQ.D2oErKma-mQrBdXxAKdH7beUkp6HS2PXUWYLCgvbP2c\",\"last_sync\":\"2026-04-27 06:28:41\",\"dateMode\":\"daily\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:29:06] 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\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring start {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring end {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:12] 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\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:30:08] 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\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30: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\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring start {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring end {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:21] 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\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Running conference:monitor:start command for activities in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: [conference:monitor:start] No activities found in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:43] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:43] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"10:25\",\"to\":\"10:30\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"00:20\",\"to\":\"00:25\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:51] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-04-27T10:32:57.492763Z\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:58] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811739,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811740,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811741,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811742,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811743,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811744,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SyncActivity] Start {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.ALERT: [SyncActivity] Failed {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] End {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Memory usage {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25149472,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] End {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Memory usage {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25527752,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQC41CLa_xP1bAbi9E2OUbAOpW..Wx961OtqTwJ3oWpOKs6yPdm47B4yKRBHbRmmJ.Lvd34sjr0gnAkV8fLb8IZWzIW93\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ALERT: [SyncActivity] Failed {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] End {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] Memory usage {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25679680,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":408,\"provider\":\"hubspot\",\"refreshToken\":\"de4e47eb985578f4218833e763e31059e88b562e87e10749b3389be2328f0aa7\",\"state\":\"connected\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:12] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Service, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BaseRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EfficientJsonParser.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProviderRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimiterInstance.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uuid, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Waveform, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Workflow, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Configuration, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Traits, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornCommandAbstract.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornPingCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSearchCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSessionCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CheckActivityLoggableCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CleanDuplicateFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FullSyncOpportunityCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LogActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageSyncStrategyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmObjectsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchOpportunityActivitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MigrateProvider.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessHubspotObjectsSyncBatches.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeDeletedOpportunitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetGovernorLimits.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNotLogged.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupActivityTypeForFollowUp.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCloseCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCopperCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCrmCommand.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupLayouts.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncAccount.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncContact.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncFieldMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotActiveDeals.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncLead.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunitiesMissingFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunity.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncProfileMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncTeamMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateOpportunitySpecifications.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dev, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddRateLimitCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixHubSpotTokens.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixMissMatchedCrmActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportCallsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MonitorSocialAccountsState.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dialers, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DTOs, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Elasticsearch, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStats, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GeckoExport, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Livestream, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Mailboxes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Migrate, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackThemes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playbooks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlists, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Postmark, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Reports, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsRetentionPolicyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsSendCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateMockAskJiminnyReportResultCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteReportCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateMarketingReport.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Team.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Usage.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Teams, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tracks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Users, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vocabulary, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Zoom, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Command.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateDatabaseUsers.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DatabaseTableCount.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteOldAiCrmNotesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteS3LeftoversCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DevPostmanCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DiarizeViaAiParticipantIdentificationCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EncryptTokensCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStatsRegenerateCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlagsHelper.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixCrossTenantIssues.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FlushRolesPermissionsCache.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateInternalWebhookToken.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GroupSetDefaultLanguageCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HelperTruncateCoachingTables.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotJournalPollingCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotWebhookServiceCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportRecording.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportUsersFromCsvFile.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IterateUsersCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyCacheClearCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyDebugCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnySetEncryptedTokenManagerModeCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyTokenInfoCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MakeSlackLiveCoachingChatNotesOn.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageScimForTeam.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkBranchForEnvironmentPipelineCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MuteOrganizerChannel.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PhpApm.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeConferences.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSoftDeletedOpportunitiesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSyncBatchesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RecalculateDealRisksCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveDeleteMarkersCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveExpiredNudgesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveUnusedParticipantSpeechesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetElasticSearch.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityCrmProviderIdCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityTypeCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RunAiCallScoringForUntypedActivitiesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SeedActivities.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNudgeExpirationWarningsCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncActivity.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TrackImported.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"WhichWorkerIsWorkingOnWhichJob.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Scheduling, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kernel.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Contracts, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Http, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Requests, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ApiResponse.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitInterface.php","depth":10,"on_screen":false,"role_description":"text"}]...
|
2832706491500642659
|
-8151256403349000395
|
visual_change
|
accessibility
|
NULL
|
This view is read-only
text/html
text/html
text/ht This view is read-only
text/html
text/html
text/html
Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {"host":"docker_lamp_1"} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {"host":"docker_lamp_1","events":2} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {"isMonday":true,"isWeekend":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {"offset":"","jiminny_team_id":1} {"correlation_id":"b453f0f4-0d2c-491c-a935-14c60265ec18","trace_id":"a50202d6-5601-46b4-a02e-cf5ac8d061fa"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {"url":"[URL_WITH_CREDENTIALS] {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {"inbox_id":212} {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:27:04] 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":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring start {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring end {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:08] 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":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:08] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:14] local.INFO: [Jiminny\Jobs\Mailbox\CreateBatches] processed 2 inboxes and created 1 batches {"userId":null,"batchSize":30,"maxBatches":1000} {"correlation_id":"98416296-43c2-4834-927e-4a0e86dc99cf","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:28:04] 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":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"dfad8817-1dde-4622-8320-f5fc1616a8fe","trace_id":"d21232b2-60fb-4ca0-838c-ba42e61593f9"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring start {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring end {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"de768781-5c05-418c-baf3-79f77a8c8694","trace_id":"fec74062-3f6d-4cfc-968a-1bb285496db3"}
[2026-04-27 10:28:12] 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":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposi...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2702
|
112
|
24
|
2026-05-07T11:36:17.252573+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153777252_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {"host":"docker_lamp_1"} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {"host":"docker_lamp_1","events":2} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {"isMonday":true,"isWeekend":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {"offset":"","jiminny_team_id":1} {"correlation_id":"b453f0f4-0d2c-491c-a935-14c60265ec18","trace_id":"a50202d6-5601-46b4-a02e-cf5ac8d061fa"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {"url":"[URL_WITH_CREDENTIALS] {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {"inbox_id":212} {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:27:04] 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":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring start {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring end {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:08] 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":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:08] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:14] local.INFO: [Jiminny\Jobs\Mailbox\CreateBatches] processed 2 inboxes and created 1 batches {"userId":null,"batchSize":30,"maxBatches":1000} {"correlation_id":"98416296-43c2-4834-927e-4a0e86dc99cf","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:28:04] 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":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"dfad8817-1dde-4622-8320-f5fc1616a8fe","trace_id":"d21232b2-60fb-4ca0-838c-ba42e61593f9"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring start {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring end {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"de768781-5c05-418c-baf3-79f77a8c8694","trace_id":"fec74062-3f6d-4cfc-968a-1bb285496db3"}
[2026-04-27 10:28:12] 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":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framewo...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {\"isMonday\":true,\"isWeekend\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":273.7,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [Send Report Not Generated Mail] Email sent {\"uuid\":\"cd5b5081-22f8-4d1a-9d6f-f82aca1121f6\",\"email\":\"lukas.kovalik@jiminny.com\"} {\"correlation_id\":\"4255983f-07a4-4872-9ea6-5491fa32bdd5\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Inbox service] Skipping METADATA SYNC for inbox 59 due to unauthorized access to the mailbox {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] Performing incremental sync for inbox 212 using history ID: @1777284288 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:27:04] 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\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring start {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring end {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:08] 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\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:14] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 1 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"98416296-43c2-4834-927e-4a0e86dc99cf\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:28:04] 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\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"dfad8817-1dde-4622-8320-f5fc1616a8fe\",\"trace_id\":\"d21232b2-60fb-4ca0-838c-ba42e61593f9\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"de768781-5c05-418c-baf3-79f77a8c8694\",\"trace_id\":\"fec74062-3f6d-4cfc-968a-1bb285496db3\"}\n[2026-04-27 10:28:12] 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\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"d8709bb9-0f8c-4798-a8c7-e68e24ab6265\",\"trace_id\":\"c5bea1cc-53fe-4bda-a8e2-bec002030bc2\"}\n[2026-04-27 10:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: Processing email batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce788ac964bbf\",\"from\":\"Nikolay Yankov <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"message_id\":\"<jiminny/app/pull/11998/before/2b8a97d6be8b053441d5a845b72dd04803cbb53c/after/8a68a94d39cc76ae546e19f354e8b0dabd577314@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce7883f2fea19\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"message_id\":\"<d70689e8-7b14-42ac-a3bf-5f1e81da943e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce761cb049fba\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"message_id\":\"<017f0e66-9989-446e-9788-7ae83a99137e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce74ed398d2c1\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Lukas Kovalik <kovaliklukas@gmail.com>, Author <author@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"message_id\":\"<jiminny/app/pull/12016/c4326142661@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce713dbecf41f\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"message_id\":\"<jiminny/infrastructure/pull/725/c4326108250@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce708ed8b5acb\",\"from\":\"Veselin Kulov <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"message_id\":\"<jiminny/infrastructure/pull/725/before/186952cc584428fcca17aa364f6cc535d65e804a/after/078246a06b25fb93ad61c7f14472ecb755483f4d@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce6eea657f2fa\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"message_id\":\"<jiminny/app/pull/11998/c4326088534@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Deleting successfully processed batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:17] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"46b082fc-5a28-4cc4-8bd1-c670f8662e7f\",\"trace_id\":\"8ae7dc26-61c7-42e4-8617-0b85f53e54c1\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] 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\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.NOTICE: Calendar sync start {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 2be54cfa-1691-4388-a284-754fdf414400 Correlation ID: 9a934049-899b-45c5-99a5-15c9c470d8a2 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"2be54cfa-1691-4388-a284-754fdf414400\\\",\\\"correlation_id\\\":\\\"9a934049-899b-45c5-99a5-15c9c470d8a2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 27729e9a-0943-41bd-9752-4ec9a8444500 Correlation ID: 824a9761-9387-4c53-ae31-5c242b1d4298 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"27729e9a-0943-41bd-9752-4ec9a8444500\\\",\\\"correlation_id\\\":\\\"824a9761-9387-4c53-ae31-5c242b1d4298\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1502,\"provider\":\"google\",\"refreshToken\":\"d417c92ebaa137295a04675f715d0511ae8acac9d779b102eac50d6300116d3e\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1502,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] 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: 73b326f0-9fcd-4ff5-ad61-edd6a0504500 Correlation ID: 6bd8fef3-b24c-46d8-8bba-a9b2ab41c108 Timestamp: 2026-04-27 10:28:28Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:28Z\\\",\\\"trace_id\\\":\\\"73b326f0-9fcd-4ff5-ad61-edd6a0504500\\\",\\\"correlation_id\\\":\\\"6bd8fef3-b24c-46d8-8bba-a9b2ab41c108\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: c5f1db6f-22fb-41cc-bd89-c46497634200 Correlation ID: 7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"c5f1db6f-22fb-41cc-bd89-c46497634200\\\",\\\"correlation_id\\\":\\\"7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: 8d6e82ba-88c3-4ec9-95ee-a4a905803900 Correlation ID: 6ede51d5-704d-4e24-9fb7-88f69f3da5db Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"8d6e82ba-88c3-4ec9-95ee-a4a905803900\\\",\\\"correlation_id\\\":\\\"6ede51d5-704d-4e24-9fb7-88f69f3da5db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1115,\"provider\":\"google\",\"refreshToken\":\"356b60f12e262a5e24d3042386ef47d6a6cfe3074c242f4426edcec8646192b1\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1115,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1421,\"provider\":\"office\",\"refreshToken\":\"9e3e287aa0cc900726e3d4eead2600fb64aa34b437790f6a1fd8048306cb2db9\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Refresh token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1421,\"provider\":\"office\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] 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\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCMx0nGVsTQEodM41jSlu-5eY2JUTYHZXE-bDro2tnepp8q_Vq3ITNS8CkncjGjcBfaHkza05dCTIlaXcSIoPNGOl9vNckYy_ZhVgE1SWvgRcCeVT-SyogdB9hP-oaWbH9r51OQrrzAwmalK5aCksyUVh9jG_A-mGVzRPnUB4SKa77o5P659lpx1egON48YU2tQ.D2oErKma-mQrBdXxAKdH7beUkp6HS2PXUWYLCgvbP2c\",\"last_sync\":\"2026-04-27 06:28:41\",\"dateMode\":\"daily\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:29:06] 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\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring start {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring end {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:12] 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\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:30:08] 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\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30: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\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring start {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring end {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:21] 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\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Running conference:monitor:start command for activities in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: [conference:monitor:start] No activities found in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:43] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:43] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"10:25\",\"to\":\"10:30\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"00:20\",\"to\":\"00:25\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:51] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-04-27T10:32:57.492763Z\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:58] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811739,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811740,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811741,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811742,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811743,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811744,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SyncActivity] Start {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.ALERT: [SyncActivity] Failed {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] End {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Memory usage {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25149472,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] End {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Memory usage {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25527752,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQC41CLa_xP1bAbi9E2OUbAOpW..Wx961OtqTwJ3oWpOKs6yPdm47B4yKRBHbRmmJ.Lvd34sjr0gnAkV8fLb8IZWzIW93\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ALERT: [SyncActivity] Failed {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] End {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] Memory usage {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25679680,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":408,\"provider\":\"hubspot\",\"refreshToken\":\"de4e47eb985578f4218833e763e31059e88b562e87e10749b3389be2328f0aa7\",\"state\":\"connected\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:12] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}","depth":4,"bounds":{"left":0.52859044,"top":0.0726257,"width":0.45977393,"height":0.9273743},"on_screen":true,"value":"[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7b1ea2fb-8f36-4897-9970-91b4abba2cc7\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {\"isMonday\":true,\"isWeekend\":false,\"isFirstDayOfMonth\":false,\"currentMonth\":4,\"isQuarterlyMonth\":true} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {\"reportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"teamId\":1,\"frequency\":\"weekly\",\"type\":\"ask_jiminny\"} {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {\"correlation_id\":\"d3f79034-397b-4936-9305-bdd99b23f2dd\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":56,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":273.7,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"b453f0f4-0d2c-491c-a935-14c60265ec18\",\"trace_id\":\"a50202d6-5601-46b4-a02e-cf5ac8d061fa\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"c6277ddd-09a6-4b04-8d42-bd395fc07d0a\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Started {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\"} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport] Fetched activity IDs for saved search {\"saved_search_id\":1977,\"user_id\":143,\"activity_count\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Fetched activity IDs {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Not enough activities, skipped {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"activityCount\":0} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [AskJiminnyReport:Generate] Dispatched not-generated notifications {\"automatedReportUuid\":\"4f6ca2b5-1993-48aa-99ad-b66f19f15d43\",\"recipientsCount\":1} {\"correlation_id\":\"f1d3e4df-9591-487f-ac2a-a43ddb89a466\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:27] local.INFO: [Send Report Not Generated Mail] Email sent {\"uuid\":\"cd5b5081-22f8-4d1a-9d6f-f82aca1121f6\",\"email\":\"lukas.kovalik@jiminny.com\"} {\"correlation_id\":\"4255983f-07a4-4872-9ea6-5491fa32bdd5\",\"trace_id\":\"aa0f8702-73eb-4947-a765-2e89244599c3\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Inbox service] Skipping METADATA SYNC for inbox 59 due to unauthorized access to the mailbox {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":59} {\"correlation_id\":\"280d5bf6-c34c-45ea-bea7-05d9c028e9d3\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync start {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] Performing incremental sync for inbox 212 using history ID: @1777284288 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {\"inbox_id\":212} {\"correlation_id\":\"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4\",\"trace_id\":\"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1\"}\n[2026-04-27 10:27:04] 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\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f55fbf77-7e5f-4c87-accd-011697ca0fa3\",\"trace_id\":\"9bd1c431-26de-4e31-83d8-3129ed9b3b79\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"da241053-5f3b-4d02-9504-4ef2987cdbd7\",\"trace_id\":\"364d3fc7-0f11-40c6-8697-d18eff2c4cb6\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring start {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:06] local.NOTICE: Monitoring end {\"correlation_id\":\"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e\",\"trace_id\":\"cb504e84-d924-43a0-82d5-faf1840134ba\"}\n[2026-04-27 10:27:08] 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\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b8652f0d-1cee-44ae-a0ea-3d3137a882c1\",\"trace_id\":\"7fe19940-0813-4199-a2a8-958de09bd0ee\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"06190462-0f47-403f-8e95-ad5d05bff1fe\",\"trace_id\":\"4aa6bed6-05a1-4661-a9c9-edadc61d84ac\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b561c5d5-e6e3-4091-9517-b0458190a360\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:27:14] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 1 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"98416296-43c2-4834-927e-4a0e86dc99cf\",\"trace_id\":\"bf9679fb-b785-40cb-b814-04a1dc385a1c\"}\n[2026-04-27 10:28:04] 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\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:04] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac\",\"trace_id\":\"25af5e8c-a148-493e-ac9f-4d53f1960177\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a\",\"trace_id\":\"c03103a3-9ab8-480b-9753-7c7c6488f246\"}\n[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"dfad8817-1dde-4622-8320-f5fc1616a8fe\",\"trace_id\":\"d21232b2-60fb-4ca0-838c-ba42e61593f9\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring start {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:09] local.NOTICE: Monitoring end {\"correlation_id\":\"93744c67-4444-41ff-aaad-6aa9cafe294e\",\"trace_id\":\"80e396b0-b1ac-4ec8-838c-00021f98072c\"}\n[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"de768781-5c05-418c-baf3-79f77a8c8694\",\"trace_id\":\"fec74062-3f6d-4cfc-968a-1bb285496db3\"}\n[2026-04-27 10:28:12] 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\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"907d15ed-8c84-41a1-a473-590eebb55dbc\",\"trace_id\":\"ec7bd353-516e-491d-89dd-de44136c1672\"}\n[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"d8709bb9-0f8c-4798-a8c7-e68e24ab6265\",\"trace_id\":\"c5bea1cc-53fe-4bda-a8e2-bec002030bc2\"}\n[2026-04-27 10:28:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: Processing email batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1354,\"provider\":\"google\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:14] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce788ac964bbf\",\"from\":\"Nikolay Yankov <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce788ac964bbf\",\"message_id\":\"<jiminny/app/pull/11998/before/2b8a97d6be8b053441d5a845b72dd04803cbb53c/after/8a68a94d39cc76ae546e19f354e8b0dabd577314@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce7883f2fea19\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce7883f2fea19\",\"message_id\":\"<d70689e8-7b14-42ac-a3bf-5f1e81da943e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce761cb049fba\",\"from\":\"The Jiminny Team <no-reply@dev.jiminny.com>\",\"to\":\"lukas.kovalik@jiminny.com\",\"cc\":null} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"no-reply@dev.jiminny.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce761cb049fba\",\"message_id\":\"<017f0e66-9989-446e-9788-7ae83a99137e@mtasv.net>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce74ed398d2c1\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Lukas Kovalik <kovaliklukas@gmail.com>, Author <author@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce74ed398d2c1\",\"message_id\":\"<jiminny/app/pull/12016/c4326142661@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce713dbecf41f\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce713dbecf41f\",\"message_id\":\"<jiminny/infrastructure/pull/725/c4326108250@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce708ed8b5acb\",\"from\":\"Veselin Kulov <notifications@github.com>\",\"to\":\"\\\"jiminny/infrastructure\\\" <infrastructure@noreply.github.com>\",\"cc\":\"Push <push@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce708ed8b5acb\",\"message_id\":\"<jiminny/infrastructure/pull/725/before/186952cc584428fcca17aa364f6cc535d65e804a/after/078246a06b25fb93ad61c7f14472ecb755483f4d@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Processing an email from inbox batch {\"batch\":98446,\"inbox_id\":212,\"email\":\"lukas.kovalik@jiminny.com\",\"email_id\":\"19dce6eea657f2fa\",\"from\":\"\\\"sonarqubecloud[bot]\\\" <notifications@github.com>\",\"to\":\"\\\"jiminny/app\\\" <app@noreply.github.com>\",\"cc\":\"Subscribed <subscribed@noreply.github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsResolver] The sender email is blacklisted, skipping {\"email\":\"notifications@github.com\",\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"team_id\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailImport\\ParticipantsValidator] Email participants are less than 2 {\"inbox_id\":212,\"message_provider_id\":\"19dce6eea657f2fa\",\"message_id\":\"<jiminny/app/pull/11998/c4326088534@github.com>\"} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Deleting successfully processed batch 98446 for inbox 212 {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":1} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5fb3c8c8-3160-4964-ac56-02d6e2cb998c\",\"trace_id\":\"3a6d5e64-ecf2-4700-98cd-3edd56a1dc34\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:26:00, 2026-04-27 10:28:00] {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ef5b3e1a-a06a-4527-9120-b2adfe4e1846\",\"trace_id\":\"d932d64a-3f63-4aca-8abf-bb13e0892cba\"}\n[2026-04-27 10:28:17] local.ERROR: Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. {\"exception\":\"[object] (Illuminate\\\\Contracts\\\\Container\\\\BindingResolutionException(code: 0): Target class [Jiminny\\\\Repositories\\\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#39 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#40 {main}\n\n[previous exception] [object] (ReflectionException(code: -1): Class \\\"Jiminny\\\\Repositories\\\\AjReportsRepository\\\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)\n[stacktrace]\n#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\\\\\Reposit...')\n#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Reposit...')\n#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Reposit...', Array, true)\n#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Reposit...', Array)\n#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Reposit...', Array)\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Reposit...')\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\\\Container\\\\Container->resolveClass(Object(ReflectionParameter))\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\\\Container\\\\Container->resolveDependencies(Array)\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\\\Container\\\\Container->build('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\\\Container\\\\Container->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array, true)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\\\Foundation\\\\Application->resolve('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\\\Container\\\\Container->make('Jiminny\\\\\\\\Http\\\\\\\\Co...', Array)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\\\Foundation\\\\Application->make('Jiminny\\\\\\\\Http\\\\\\\\Co...')\n#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\\\Routing\\\\Route->getController()\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\\\Routing\\\\Route->controllerMiddleware()\n#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\\\Routing\\\\Route->gatherMiddleware()\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\\\Routing\\\\Router->gatherRouteMiddleware(Object(Illuminate\\\\Routing\\\\Route))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\\\Routing\\\\Router->runRouteWithinStack(Object(Illuminate\\\\Routing\\\\Route), Object(Illuminate\\\\Http\\\\Request))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\\\Routing\\\\Router->runRoute(Object(Illuminate\\\\Http\\\\Request), Object(Illuminate\\\\Routing\\\\Route))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\\\Routing\\\\Router->dispatchToRoute(Object(Illuminate\\\\Http\\\\Request))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\\\Routing\\\\Router->dispatch(Object(Illuminate\\\\Http\\\\Request))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\\\Foundation\\\\Http\\\\Kernel->Illuminate\\\\Foundation\\\\Http\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\\\Debugbar\\\\Middleware\\\\InjectDebugbar->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Http\\\\Middleware\\\\HandleCors->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TransformsRequest->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\TrimStrings->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\\\SecureHeaders\\\\SecureHeadersMiddleware->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\\\Http\\\\Middleware\\\\SentryContext->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\\\Foundation\\\\Http\\\\Middleware\\\\InvokeDeferredCallbacks->handle(Object(Illuminate\\\\Http\\\\Request), Object(Closure))\n#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\\\Pipeline\\\\Pipeline->Illuminate\\\\Pipeline\\\\{closure}(Object(Illuminate\\\\Http\\\\Request))\n#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\\\Pipeline\\\\Pipeline->then(Object(Closure))\n#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\\\Foundation\\\\Http\\\\Kernel->sendRequestThroughRouter(Object(Illuminate\\\\Http\\\\Request))\n#40 /home/jiminny/public/index.php(51): Illuminate\\\\Foundation\\\\Http\\\\Kernel->handle(Object(Illuminate\\\\Http\\\\Request))\n#41 {main}\n\"} {\"correlation_id\":\"46b082fc-5a28-4cc4-8bd1-c670f8662e7f\",\"trace_id\":\"8ae7dc26-61c7-42e4-8617-0b85f53e54c1\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] 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\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.NOTICE: Calendar sync start {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e155562a-84a1-45f5-8897-1779f96886f6\",\"trace_id\":\"12141267-1032-479b-aab5-e17e133007d9\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:19] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:20] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:21] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:22] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:23] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:24] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:25] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 2be54cfa-1691-4388-a284-754fdf414400 Correlation ID: 9a934049-899b-45c5-99a5-15c9c470d8a2 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"2be54cfa-1691-4388-a284-754fdf414400\\\",\\\"correlation_id\\\":\\\"9a934049-899b-45c5-99a5-15c9c470d8a2\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] 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: 27729e9a-0943-41bd-9752-4ec9a8444500 Correlation ID: 824a9761-9387-4c53-ae31-5c242b1d4298 Timestamp: 2026-04-27 10:28:26Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:26Z\\\",\\\"trace_id\\\":\\\"27729e9a-0943-41bd-9752-4ec9a8444500\\\",\\\"correlation_id\\\":\\\"824a9761-9387-4c53-ae31-5c242b1d4298\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:26] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1502,\"provider\":\"google\",\"refreshToken\":\"d417c92ebaa137295a04675f715d0511ae8acac9d779b102eac50d6300116d3e\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1502,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] 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: 73b326f0-9fcd-4ff5-ad61-edd6a0504500 Correlation ID: 6bd8fef3-b24c-46d8-8bba-a9b2ab41c108 Timestamp: 2026-04-27 10:28:28Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:28Z\\\",\\\"trace_id\\\":\\\"73b326f0-9fcd-4ff5-ad61-edd6a0504500\\\",\\\"correlation_id\\\":\\\"6bd8fef3-b24c-46d8-8bba-a9b2ab41c108\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: c5f1db6f-22fb-41cc-bd89-c46497634200 Correlation ID: 7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"c5f1db6f-22fb-41cc-bd89-c46497634200\\\",\\\"correlation_id\\\":\\\"7b4df0cc-bf50-4017-9b1a-e25ffb6fe9bf\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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\":\"8a9488be-81de-40f5-8554-2503eec42566\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:29] 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: 8d6e82ba-88c3-4ec9-95ee-a4a905803900 Correlation ID: 6ede51d5-704d-4e24-9fb7-88f69f3da5db Timestamp: 2026-04-27 10:28:29Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-04-27 10:28:29Z\\\",\\\"trace_id\\\":\\\"8d6e82ba-88c3-4ec9-95ee-a4a905803900\\\",\\\"correlation_id\\\":\\\"6ede51d5-704d-4e24-9fb7-88f69f3da5db\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1115,\"provider\":\"google\",\"refreshToken\":\"356b60f12e262a5e24d3042386ef47d6a6cfe3074c242f4426edcec8646192b1\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:30] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1115,\"provider\":\"google\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1421,\"provider\":\"office\",\"refreshToken\":\"9e3e287aa0cc900726e3d4eead2600fb64aa34b437790f6a1fd8048306cb2db9\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:31] 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\":\"8fdf411d-8a4f-46c1-a46a-cc84f3c2f2b1\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountObserver] Refresh token was modified, encrypting {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1421,\"provider\":\"office\",\"state\":\"connected\"} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:32] 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\":\"4a0d5722-0aa0-4768-bf49-106fc050bf48\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCMx0nGVsTQEodM41jSlu-5eY2JUTYHZXE-bDro2tnepp8q_Vq3ITNS8CkncjGjcBfaHkza05dCTIlaXcSIoPNGOl9vNckYy_ZhVgE1SWvgRcCeVT-SyogdB9hP-oaWbH9r51OQrrzAwmalK5aCksyUVh9jG_A-mGVzRPnUB4SKa77o5P659lpx1egON48YU2tQ.D2oErKma-mQrBdXxAKdH7beUkp6HS2PXUWYLCgvbP2c\",\"last_sync\":\"2026-04-27 06:28:41\",\"dateMode\":\"daily\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:28:34] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"52b2ff86-82c7-4ed0-b528-35d7d00d69f0\",\"trace_id\":\"bc31793c-511e-425e-b60b-476026a43831\"}\n[2026-04-27 10:29:06] 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\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"7e95c05b-50c8-4e0b-a01c-efeed6af544c\",\"trace_id\":\"07db4d4f-d6e7-44d9-8e54-4f038ea69afb\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e6e7fbf8-6e80-4529-858e-a4e95d0d3805\",\"trace_id\":\"f91a115a-9fdc-46ce-b9f2-13a1e6d0e325\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring start {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:10] local.NOTICE: Monitoring end {\"correlation_id\":\"211e5e36-1c6c-4198-b928-d2f1525ab0ea\",\"trace_id\":\"c26e980d-6e32-426d-9667-f1d902432b8d\"}\n[2026-04-27 10:29:12] 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\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"684bdf68-76ee-42dd-9d97-62f414a33b1b\",\"trace_id\":\"284c98ef-5b7f-4572-996c-72c5bc2995f9\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:29:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6e74b148-ae54-42ff-ab72-2ded18d10ff4\",\"trace_id\":\"d05bc9a0-450c-45b2-be3b-225a551ed909\"}\n[2026-04-27 10:30:08] 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\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30:08] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"24b807e0-b413-4ecc-a89b-365280be88ac\",\"trace_id\":\"542ac70c-6756-4409-8224-0844a7576d9c\"}\n[2026-04-27 10:30: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\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"2711ca04-b4a7-4f3a-b0b2-5826fea4da3b\",\"trace_id\":\"2613ffca-f181-4d91-ad55-ec1bdd97ee7e\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring start {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:17] local.NOTICE: Monitoring end {\"correlation_id\":\"e1537475-8481-4144-b9f5-596ea8034b8a\",\"trace_id\":\"f5561d0a-0cb7-4c72-8bde-cce617183529\"}\n[2026-04-27 10:30:21] 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\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8a911b60-16a0-416e-934e-3b7e0280e452\",\"trace_id\":\"3c2bbdd6-985d-4624-b42b-fd9cc2b4756f\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:25] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f62efaa6-201f-43b2-bf45-9619982dffb1\",\"trace_id\":\"f1ad3d60-af4e-4427-9d77-6aa71cfed172\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Running conference:monitor:count command for activities in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: [conference:monitor:count] No activities found in (2026-04-27 10:28:00, 2026-04-27 10:30:00] {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:30] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cf6dc132-ecb8-4efc-bead-06bf1c96cae6\",\"trace_id\":\"9e2b073b-940d-498a-babe-115f633dd954\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:32] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f455260d-d1c1-456f-8937-d4bb026d9078\",\"trace_id\":\"f8c2e585-0137-4103-9e16-1b4ce05df2ef\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:35] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"be7d3c6a-664a-42e8-a4b3-cbd021296404\",\"trace_id\":\"0b250827-4c02-4237-8b34-e5ddd004eba1\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Running pre-meeting notification command {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-notification\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"5d8fbdf6-2737-4d73-babe-699c88279e1f\",\"trace_id\":\"0882a963-b02f-4049-b670-12b92f80829a\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Running conference:monitor:start command for activities in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: [conference:monitor:start] No activities found in (2026-04-27 10:20:00, 2026-04-27 10:25:00] {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:start\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"e8968e47-9acd-4bd4-9e7d-3c415f46df05\",\"trace_id\":\"287fba85-f395-41ab-813d-49cb5d0ea06d\"}\n[2026-04-27 10:30:43] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:43] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesEnded {\"from\":\"10:25\",\"to\":\"10:30\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: conference:monitor:end:Jiminny\\Console\\Commands\\Activities\\MonitorMeetingEndCommand::logActivitiesWithUnfinishedSession {\"from\":\"00:20\",\"to\":\"00:25\"} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:end\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b87eb011-7108-46bd-aa90-975ebe41f483\",\"trace_id\":\"1f54c2a6-fad1-4804-87d8-4972a378e91b\"}\n[2026-04-27 10:30:51] local.NOTICE: Repairing HubSpot tokens start {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: Trying to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:51] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":59,\"provider\":\"hubspot\",\"refreshToken\":\"97b78f6e2cc49965c00c2492b602b02708b1392551e6b3f113fbaa48992af90b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":59,\"updated_at\":\"2025-10-03 09:32:05\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":306,\"provider\":\"hubspot\",\"refreshToken\":\"6fa6aa8cc641d131231acc3470f5c03cb3b07b2e580fb18f8acb3b1dbb72549b\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":306,\"updated_at\":\"2023-11-27 09:30:03\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: Trying to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1372,\"provider\":\"hubspot\",\"refreshToken\":\"9aa73948c761da29dce46c177cf9aee1fde483a44169ca38723f9f0597d7a8c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.ERROR: Failed to refresh HubSpot token {\"account_id\":1372,\"updated_at\":\"2025-10-02 14:47:06\",\"reason\":\"missing or invalid refresh token\",\"previous\":\"\"} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:52] local.NOTICE: Repairing HubSpot tokens end {\"total\":3,\"fixed\":0,\"failed\":3} {\"correlation_id\":\"2e6dfa3d-f07d-4eee-8232-6138133e2fbb\",\"trace_id\":\"41dea86e-0ecc-414e-8303-e60d8ae09976\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"jiminny:transcription:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a6655a64-38ad-4c65-96f1-f5a294dbb343\",\"trace_id\":\"52cdca32-c0c8-4215-81a2-4263bbde2158\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Command] Starting polling service {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Service starting {\"memory_limit\":\"256M\",\"max_execution_time\":\"0\",\"initial_memory_mb\":62.0} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Acquired polling lock {\"expires_at\":\"2026-04-27T10:32:57.492763Z\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:30:57] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:pre-meeting-reminder\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ec2d49dd-ab36-4f4e-8a60-7bf5ea384cac\",\"trace_id\":\"b3e89960-9a27-4bf7-8b55-95f5704f9a10\"}\n[2026-04-27 10:30:58] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:02] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:reset-governor\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f261cd75-c916-4f21-b4f4-856fe0ed315c\",\"trace_id\":\"6db8819d-ffaa-4fe8-8ccd-0dcc326b8cdc\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:03] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811739,\"provider\":\"twilio-flex\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811740,\"provider\":\"xant\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811741,\"provider\":\"apollo\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811742,\"provider\":\"groove\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811743,\"provider\":\"twilio-video\",\"team\":\"jiminny\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Dispatching activity sync job {\"import_id\":811744,\"provider\":\"hubspot\",\"team\":\"hubspot\"} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f9151ed9-60aa-4b2b-b962-9eab5f75405b\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.WARNING: [Salesforce] Account not connected for user {\"userId\":\"cdf8b554-d951-4758-bc2b-c1b85d1cd0b9\",\"account\":null} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"salesforce\",\"crm_owner\":3,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [CrmOwnerResolver] TeamMember found with active crm connection {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1194,\"provider\":\"twilio-flex\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.INFO: [SyncActivity] Start {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:07] local.NOTICE: [TwilioFlex] Calls import start {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.ALERT: [SyncActivity] Failed {\"import_id\":811739,\"provider\":\"twilio-flex\",\"provider_id\":317,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"[HTTP 401] Unable to fetch page: Authenticate\",\"file\":\"/home/jiminny/vendor/twilio/sdk/src/Twilio/Page.php\",\"line\":60} {\"correlation_id\":\"e7a521c5-fad4-48c2-b880-d572b5d237f4\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal Polling] Getting offset from database {\"offset\":\"\",\"jiminny_team_id\":1} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:08] local.INFO: [HubSpot Journal API] Fetching latest journal entry {\"url\":\"https://api.hubapi.com/webhooks/v4/journal/latest\"} {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f928e93-b17e-4518-a1b9-a8280d1d95e8\",\"trace_id\":\"ba203ed6-fe98-4fd7-aa62-8de9fb8ffeca\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT Playbooks_Call_Date__c,Playbooks_Call_Recording__c,CreatedDate,TaskSubtype,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+Playbooks_Call_Date__c%2CPlaybooks_Call_Recording__c%2CCreatedDate%2CTaskSubtype%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:fail-stalled\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0d30446d-c469-4ade-80dc-f57f997ea610\",\"trace_id\":\"61eea326-3f40-4a50-a5e6-bddd7fa25d27\"}\n[2026-04-27 10:31:09] local.INFO: [Xant (InsideSales)] No calls found. {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] End {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Memory usage {\"import_id\":811740,\"provider\":\"xant\",\"provider_id\":161,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25149472,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"73848601-79bb-4161-91ae-37acf4385d24\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [SyncActivity] Start {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT AccountId,CreatedDate,TaskSubtype,CallType,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:09] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+AccountId%2CCreatedDate%2CTaskSubtype%2CCallType%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Apollo] No calls found. {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] End {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Memory usage {\"import_id\":811741,\"provider\":\"apollo\",\"provider_id\":441,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25527752,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"11c977fb-9603-4dde-a4d7-f821d47fcdc9\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"\n SELECT call_recording_url__c,TaskSubtype,CreatedDate,CallType,CallDurationInSeconds,Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type\n FROM Task\n WHERE IsDeleted = false\n AND LastModifiedDate >= :from\n AND LastModifiedDate <= :to\n ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ERROR: [Salesforce] Request exception [400] \nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names. {\"url\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=%0A++++++++++++SELECT+call_recording_url__c%2CTaskSubtype%2CCreatedDate%2CCallType%2CCallDurationInSeconds%2CId%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%0A++++++++++++++FROM+Task%0A+++++++++++++WHERE+IsDeleted+%3D+false%0A+++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A+++++++++++++++AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z%0A++++++++++ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000\",\"data\":{\"headers\":{\"Authorization\":\"Bearer 00D2g0000008hH4!AQEAQC41CLa_xP1bAbi9E2OUbAOpW..Wx961OtqTwJ3oWpOKs6yPdm47B4yKRBHbRmmJ.Lvd34sjr0gnAkV8fLb8IZWzIW93\"}},\"response\":{\"GuzzleHttp\\\\Psr7\\\\Stream\":\"[{\\\"message\\\":\\\"\\\\nSELECT call_recording_url__c,TaskSubtype\\\\n ^\\\\nERROR at Row:1:Column:8\\\\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\\\",\\\"errorCode\\\":\\\"INVALID_FIELD\\\"}]\"},\"fields\":[]} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.ALERT: [SyncActivity] Failed {\"import_id\":811742,\"provider\":\"groove\",\"provider_id\":228,\"team\":\"jiminny\",\"team_id\":1,\"reason\":\"\nSELECT call_recording_url__c,TaskSubtype\n ^\nERROR at Row:1:Column:8\nNo such column 'call_recording_url__c' on entity 'Task'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.\",\"file\":\"/home/jiminny/app/Services/Crm/Salesforce/Client.php\",\"line\":564} {\"correlation_id\":\"856f8092-8c5e-409d-8aa6-ffb8efd8bfa1\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"salesforce\",\"crm_owner\":143,\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1500,\"provider\":\"salesforce\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [SyncActivity] Start {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Performing query {\"query\":\"SELECT Id,OwnerId,WhoId,WhatId,Priority,ActivityDate,Subject,Description,Status,Type,twilio_call_sid__c,Lead_UUID__c,Opportunity__c\n FROM Task\n WHERE Type = 'Video'\n AND isClosed = true\n AND IsDeleted = false\n AND LastModifiedDate >= :from\n AND twilio_call_sid__c != NULL AND LastModifiedDate <= :to ORDER BY LastModifiedDate ASC\n LIMIT :limit\",\"params\":{\"from\":\"2026-04-27T10:14:00Z\",\"to\":\"2026-04-27T10:30:00Z\",\"ownerId\":null,\"subType\":null,\"limit\":5000}} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:10] local.INFO: [Salesforce] Sending request {\"endpoint\":\"https://jiminny--stagingenv.sandbox.my.salesforce.com/services/data/v50.0/query/?q=SELECT+Id%2COwnerId%2CWhoId%2CWhatId%2CPriority%2CActivityDate%2CSubject%2CDescription%2CStatus%2CType%2Ctwilio_call_sid__c%2CLead_UUID__c%2COpportunity__c%0A++++++++++++++FROM+Task%0A++++++++++++WHERE+Type+%3D+%27Video%27%0A++++++++++++++AND+isClosed+%3D+true%0A++++++++++++++AND+IsDeleted+%3D+false%0A++++++++++++++AND+LastModifiedDate+%3E%3D+2026-04-27T10%3A14%3A00Z%0A++++++++++++++AND+twilio_call_sid__c+%21%3D+NULL+AND+LastModifiedDate+%3C%3D+2026-04-27T10%3A30%3A00Z+ORDER+BY+LastModifiedDate+ASC%0A+++++++++++++LIMIT+5000 GET\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [Twilio Video] No calls found. {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] End {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SyncActivity] Memory usage {\"import_id\":811743,\"provider\":\"twilio-video\",\"provider_id\":243,\"team\":\"jiminny\",\"team_id\":1,\"memory_usage\":25679680,\"memory_real_usage\":65011712,\"pid\":13616} {\"correlation_id\":\"0ec6aef6-345c-4602-b6a1-646b95e481c0\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":408,\"provider\":\"hubspot\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":408,\"provider\":\"hubspot\",\"refreshToken\":\"de4e47eb985578f4218833e763e31059e88b562e87e10749b3389be2328f0aa7\",\"state\":\"connected\"} {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:11] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:bullhorn:ping\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2085168-092a-44b0-8dd2-c8845bc36f86\",\"trace_id\":\"9445425b-6aaf-4a23-9323-ecf85939f967\"}\n[2026-04-27 10:31:12] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"3b9d7e85-b8c2-4ae2-863e-c343c277afdc\",\"trace_id\":\"dba432b8-cb4b-4e76-8684-01d96f91e453\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6881570513140039651
|
-8151256403349000395
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] STARTING Inbox Sync {"host":"docker_lamp_1"} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: [EmailSchedule] FINISHED Inbox Sync {"host":"docker_lamp_1","events":2} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:25] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:sync","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"7b1ea2fb-8f36-4897-9970-91b4abba2cc7","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Started {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Checking conditions {"isMonday":true,"isWeekend":false,"isFirstDayOfMonth":false,"currentMonth":4,"isQuarterlyMonth":true} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing daily reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 daily reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Processing weekly reports {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Found 1 weekly reports to process {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Dispatching Generate Report job for report {"reportUuid":"4f6ca2b5-1993-48aa-99ad-b66f19f15d43","teamId":1,"frequency":"weekly","type":"ask_jiminny"} {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [automated-reports] Completed {"correlation_id":"d3f79034-397b-4936-9305-bdd99b23f2dd","trace_id":"aa0f8702-73eb-4947-a765-2e89244599c3"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal Polling] Getting offset from database {"offset":"","jiminny_team_id":1} {"correlation_id":"b453f0f4-0d2c-491c-a935-14c60265ec18","trace_id":"a50202d6-5601-46b4-a02e-cf5ac8d061fa"}
[2026-04-27 10:26:26] local.INFO: [HubSpot Journal API] Fetching latest journal entry {"url":"[URL_WITH_CREDENTIALS] {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] imported 7 emails via full sync workflow for inbox 212 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Gmail] seeding inbox 212 with last message time : 2026-04-27 10:24:57 {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:26:28] local.INFO: [Sync Mailbox] Sync complete {"inbox_id":212} {"correlation_id":"d3e3eb3d-5c83-4d6e-9592-aaa38c1843a4","trace_id":"265be7c0-bf8f-4037-a0d2-57df3ef1d7e1"}
[2026-04-27 10:27:04] 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":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f55fbf77-7e5f-4c87-accd-011697ca0fa3","trace_id":"9bd1c431-26de-4e31-83d8-3129ed9b3b79"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"da241053-5f3b-4d02-9504-4ef2987cdbd7","trace_id":"364d3fc7-0f11-40c6-8697-d18eff2c4cb6"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring start {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:06] local.NOTICE: Monitoring end {"correlation_id":"ad0d871a-a1fd-4725-b2f7-ac8f0fc1857e","trace_id":"cb504e84-d924-43a0-82d5-faf1840134ba"}
[2026-04-27 10:27:08] 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":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:08] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b8652f0d-1cee-44ae-a0ea-3d3137a882c1","trace_id":"7fe19940-0813-4199-a2a8-958de09bd0ee"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:09] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"06190462-0f47-403f-8e95-ad5d05bff1fe","trace_id":"4aa6bed6-05a1-4661-a9c9-edadc61d84ac"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] STARTING batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: [EmailSchedule] FINISHED batch create {"host":"docker_lamp_1"} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:11] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:create","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"b561c5d5-e6e3-4091-9517-b0458190a360","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:27:14] local.INFO: [Jiminny\Jobs\Mailbox\CreateBatches] processed 2 inboxes and created 1 batches {"userId":null,"batchSize":30,"maxBatches":1000} {"correlation_id":"98416296-43c2-4834-927e-4a0e86dc99cf","trace_id":"bf9679fb-b785-40cb-b814-04a1dc385a1c"}
[2026-04-27 10:28:04] 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":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:04] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"5852bcf1-af92-4d24-8a2d-08a9d4a5cfac","trace_id":"25af5e8c-a148-493e-ac9f-4d53f1960177"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc1cff1f-71b4-4bb1-800f-66e27c0f2b3a","trace_id":"c03103a3-9ab8-480b-9753-7c7c6488f246"}
[2026-04-27 10:28:09] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"dfad8817-1dde-4622-8320-f5fc1616a8fe","trace_id":"d21232b2-60fb-4ca0-838c-ba42e61593f9"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring start {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:09] local.NOTICE: Monitoring end {"correlation_id":"93744c67-4444-41ff-aaad-6aa9cafe294e","trace_id":"80e396b0-b1ac-4ec8-838c-00021f98072c"}
[2026-04-27 10:28:10] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#30 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#31 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#39 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#40 {main}
[previous exception] [object] (ReflectionException(code: -1): Class \"Jiminny\\Repositories\\AjReportsRepository\" does not exist at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1122)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1122): ReflectionClass->__construct('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#3 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#4 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Reposit...', Array)
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1336): Illuminate\\Foundation\\Application->make('Jiminny\\\\Reposit...')
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1237): Illuminate\\Container\\Container->resolveClass(Object(ReflectionParameter))
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(1162): Illuminate\\Container\\Container->resolveDependencies(Array)
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Http\\\\Co...')
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Http\\\\Co...', Array, true)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Http\\\\Co...', Array)
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1058): Illuminate\\Container\\Container->make('Jiminny\\\\Http\\\\Co...', Array)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(286): Illuminate\\Foundation\\Application->make('Jiminny\\\\Http\\\\Co...')
#13 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1133): Illuminate\\Routing\\Route->getController()
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Route.php(1062): Illuminate\\Routing\\Route->controllerMiddleware()
#15 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->gatherMiddleware()
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(816): Illuminate\\Routing\\Router->gatherRouteMiddleware(Object(Illuminate\\Routing\\Route))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(800): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(764): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Routing/Router.php(753): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(200): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))
#22 /home/jiminny/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php(59): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#23 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Barryvdh\\Debugbar\\Middleware\\InjectDebugbar->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php(74): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#25 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Http\\Middleware\\HandleCors->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#26 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#27 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(51): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#29 /home/jiminny/vendor/bepsvpt/secure-headers/src/SecureHeadersMiddleware.php(18): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#30 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Bepsvpt\\SecureHeaders\\SecureHeadersMiddleware->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#31 /home/jiminny/app/Http/Middleware/SentryContext.php(60): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#32 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Jiminny\\Http\\Middleware\\SentryContext->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#33 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(109): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#34 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#35 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/InvokeDeferredCallbacks.php(22): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#36 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(219): Illuminate\\Foundation\\Http\\Middleware\\InvokeDeferredCallbacks->handle(Object(Illuminate\\Http\\Request), Object(Closure))
#37 /home/jiminny/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))
#38 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(175): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))
#39 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(144): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))
#40 /home/jiminny/public/index.php(51): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))
#41 {main}
"} {"correlation_id":"de768781-5c05-418c-baf3-79f77a8c8694","trace_id":"fec74062-3f6d-4cfc-968a-1bb285496db3"}
[2026-04-27 10:28:12] 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":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"907d15ed-8c84-41a1-a473-590eebb55dbc","trace_id":"ec7bd353-516e-491d-89dd-de44136c1672"}
[2026-04-27 10:28:13] local.ERROR: Target class [Jiminny\Repositories\AjReportsRepository] does not exist. {"exception":"[object] (Illuminate\\Contracts\\Container\\BindingResolutionException(code: 0): Target class [Jiminny\\Repositories\\AjReportsRepository] does not exist. at /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php:1124)
[stacktrace]
#0 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(933): Illuminate\\Container\\Container->build('Jiminny\\\\Reposit...')
#1 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1078): Illuminate\\Container\\Container->resolve('Jiminny\\\\Reposit...', Array, true)
#2 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(864): Illuminate\\Foundation\\Application->resolve('Jiminny\\\\Reposit...', Array)
#3 /home/jiminny/vendor/laravel/framewo...
|
2701
|
NULL
|
NULL
|
NULL
|
|
2705
|
112
|
25
|
2026-05-07T11:36:25.798933+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153785798_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavigarecoderTavsco.s°9 masterProlete PhostormVIewINavigarecoderTavsco.s°9 masterProletey(C) [EMAIL]© Uuid.php> D Traits> D UseCases› DUser› DUtils› D Validation>Ovophp nelpers.onp© InitialFrontendState.phpc) Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial› D contrib• database, m docsfront-endlangnode_modules library root› [J phpstanM oublic> D resourcesDroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aol.onppnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage→aoo> D debugbarM frameworkv Mloas.aitianoreD audio.wav= custom.loa720723727729=hubsnot-iournal-noll.log= laravel loal734 0 >740us tht is50 lhl# Support Daily - in 24 m100% CThu 7 May 14:36:25CleanShot is ready to use!= custom.log x E laravelllog x 4 SF [jiminny@localhost]A HS_Jocal [jiminny@localhost]# console [PKol)A Consoie (eUJ© RematchActivityOnCrmObjectDetach.phpDeleteCrmEntityTrait.phpAddkateLimitcommano.php© Hubspot/Client.php x © HandleRateLimit.php© SyncTeamMetadata.php©RateLimit.php© Configuration.php© RateLimitException.php©RateLimitinterface.php©) ProviderkateLimiter.phgclass Cllent extends Baseclient 1mpLements Hubspotclientintertace6826971* @return array<CrmFieldOption>public function fetch0pportunityFieldOptions(Field $field): arrayf…;* achnows badkequest* achrows hubspoctxcepc1onpublic function makeRequest(string $endpoint, Smethod = 'GET', $payload = [], ?string $queryString = null)Sendpoint = self::BASEURL . Sendpoint.1 Smethod === "GET') <felse{Sresponse = $this->getInstance()->getClient()->request($method, $endpoint, ["json'= (Snavload)ISremaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining');1 "309"Sinterval = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"$body=son decodel(scring) sresponse->gerbodyassociative: true);(Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('$max' . PHP_EOL . print_r($max, return: true));\Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('Sremaining' . PHP_EOL . print_r($remaining, return: true));uuLuminace supporc racades Loo..channel channel'custom_channel')->info('Sinterval ' . PHP_EOL . print_ r(Sinterval. return: true)):ILLuminate Support Facades Loq::channel( channel'custom_channel')->info('$body' . PHP_EOL . print_r($body, return: true));* athrows HubsnotExcentionnublic function createMeetina(arrav Spavload): Resnonse-...?...
|
NULL
|
210665823171349791
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavigarecoderTavsco.s°9 masterProlete PhostormVIewINavigarecoderTavsco.s°9 masterProletey(C) [EMAIL]© Uuid.php> D Traits> D UseCases› DUser› DUtils› D Validation>Ovophp nelpers.onp© InitialFrontendState.phpc) Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial› D contrib• database, m docsfront-endlangnode_modules library root› [J phpstanM oublic> D resourcesDroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aol.onppnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage→aoo> D debugbarM frameworkv Mloas.aitianoreD audio.wav= custom.loa720723727729=hubsnot-iournal-noll.log= laravel loal734 0 >740us tht is50 lhl# Support Daily - in 24 m100% CThu 7 May 14:36:25CleanShot is ready to use!= custom.log x E laravelllog x 4 SF [jiminny@localhost]A HS_Jocal [jiminny@localhost]# console [PKol)A Consoie (eUJ© RematchActivityOnCrmObjectDetach.phpDeleteCrmEntityTrait.phpAddkateLimitcommano.php© Hubspot/Client.php x © HandleRateLimit.php© SyncTeamMetadata.php©RateLimit.php© Configuration.php© RateLimitException.php©RateLimitinterface.php©) ProviderkateLimiter.phgclass Cllent extends Baseclient 1mpLements Hubspotclientintertace6826971* @return array<CrmFieldOption>public function fetch0pportunityFieldOptions(Field $field): arrayf…;* achnows badkequest* achrows hubspoctxcepc1onpublic function makeRequest(string $endpoint, Smethod = 'GET', $payload = [], ?string $queryString = null)Sendpoint = self::BASEURL . Sendpoint.1 Smethod === "GET') <felse{Sresponse = $this->getInstance()->getClient()->request($method, $endpoint, ["json'= (Snavload)ISremaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining');1 "309"Sinterval = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"$body=son decodel(scring) sresponse->gerbodyassociative: true);(Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('$max' . PHP_EOL . print_r($max, return: true));\Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('Sremaining' . PHP_EOL . print_r($remaining, return: true));uuLuminace supporc racades Loo..channel channel'custom_channel')->info('Sinterval ' . PHP_EOL . print_ r(Sinterval. return: true)):ILLuminate Support Facades Loq::channel( channel'custom_channel')->info('$body' . PHP_EOL . print_r($body, return: true));* athrows HubsnotExcentionnublic function createMeetina(arrav Spavload): Resnonse-...?...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2707
|
112
|
26
|
2026-05-07T11:36:29.137637+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153789137_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4800532,"height":-0.06304872},"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-8970535408088379666
|
4648522587191274515
|
click
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
2705
|
NULL
|
NULL
|
NULL
|
|
2710
|
112
|
27
|
2026-05-07T11:36:35.455775+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153795455_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-05-07 11:36:26] 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":"f71ac7c4-e9d4-46ad-be82-47e68a8f1199","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:26] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f71ac7c4-e9d4-46ad-be82-47e68a8f1199","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24412984,"real_usage":65011712,"pid":13732} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1499,"provider":"hubspot","refreshToken":"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86","state":"connected"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [SocialAccountService] Token refreshed {"socialAccountId":1499,"provider":"hubspot","state":"connected"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 11:36:26] 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\":\"f71ac7c4-e9d4-46ad-be82-47e68a8f1199\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:26] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f71ac7c4-e9d4-46ad-be82-47e68a8f1199\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24412984,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"refreshToken\":\"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86\",\"state\":\"connected\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"state\":\"connected\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] 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.65,\"average_seconds_per_request\":0.65} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":682.84} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":1640.35,\"usage\":24648760,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24623496,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] 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\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":64.6,\"usage\":24516544,\"real_usage\":65011712,\"pid\":13732,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24554912,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":81.21,\"usage\":24510344,\"real_usage\":65011712,\"pid\":13732,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24548712,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":77.05,\"usage\":24483760,\"real_usage\":65011712,\"pid\":13732,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:notify-not-logged\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cd860d87-3b37-4cc4-b4bc-7788e90ccadc\",\"trace_id\":\"ecfea9bc-0fe5-4224-ba87-0b84ebb94d35\"}\n[2026-05-07 11:36:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:notify-not-logged\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cd860d87-3b37-4cc4-b4bc-7788e90ccadc\",\"trace_id\":\"ecfea9bc-0fe5-4224-ba87-0b84ebb94d35\"}\n[2026-05-07 11:36:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}\n[2026-05-07 11:36:34] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}\n[2026-05-07 11:36:34] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}\n[2026-05-07 11:36:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}","depth":4,"bounds":{"left":0.52859044,"top":0.0726257,"width":0.45977393,"height":0.9273743},"on_screen":true,"value":"[2026-05-07 11:36:26] 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\":\"f71ac7c4-e9d4-46ad-be82-47e68a8f1199\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:26] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f71ac7c4-e9d4-46ad-be82-47e68a8f1199\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24412984,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"refreshToken\":\"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86\",\"state\":\"connected\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SocialAccountService] Token refreshed {\"socialAccountId\":1499,\"provider\":\"hubspot\",\"state\":\"connected\"} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] 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.65,\"average_seconds_per_request\":0.65} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":682.84} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":1640.35,\"usage\":24648760,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"766570a3-89a2-4427-81c2-c70a7c780ee3\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24623496,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] 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\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":64.6,\"usage\":24516544,\"real_usage\":65011712,\"pid\":13732,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"cb3b51ca-fd95-4a3f-8366-1dedc565b784\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24554912,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:28] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":81.21,\"usage\":24510344,\"real_usage\":65011712,\"pid\":13732,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"b5acfa3a-a5cb-43e0-8757-36cc6bc0da54\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24548712,\"real_usage\":65011712,\"pid\":13732} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:29] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":77.05,\"usage\":24483760,\"real_usage\":65011712,\"pid\":13732,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"a40d012c-b10d-4de7-9ea1-ef68239f712b\",\"trace_id\":\"5c938a2d-aea9-4025-a7f7-733d8e8e9711\"}\n[2026-05-07 11:36:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:notify-not-logged\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cd860d87-3b37-4cc4-b4bc-7788e90ccadc\",\"trace_id\":\"ecfea9bc-0fe5-4224-ba87-0b84ebb94d35\"}\n[2026-05-07 11:36:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:notify-not-logged\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cd860d87-3b37-4cc4-b4bc-7788e90ccadc\",\"trace_id\":\"ecfea9bc-0fe5-4224-ba87-0b84ebb94d35\"}\n[2026-05-07 11:36:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}\n[2026-05-07 11:36:34] local.INFO: [EmailSchedule] STARTING Inbox Sync {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}\n[2026-05-07 11:36:34] local.INFO: [EmailSchedule] FINISHED Inbox Sync {\"host\":\"docker_lamp_1\",\"events\":2} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}\n[2026-05-07 11:36:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a179c5a9-e51e-4929-b0a8-717068ede438\",\"trace_id\":\"e12b0454-0027-4fce-8a8f-8572b5c2a51f\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8123360485970677608
|
5514066055808292964
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
[2026-05-07 11:36:26] 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":"f71ac7c4-e9d4-46ad-be82-47e68a8f1199","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:26] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f71ac7c4-e9d4-46ad-be82-47e68a8f1199","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24412984,"real_usage":65011712,"pid":13732} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Token needs refreshing {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountService] Refreshing token from provider {"socialAccountId":1499,"provider":"hubspot","refreshToken":"96f94c623a404e02ebdbf07f1b75707bb6cdbf848cbf45d418baf608c41a8d86","state":"connected"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Saving model {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:27] local.INFO: [SocialAccountObserver] Access token was modified, encrypting {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [SocialAccountService] Token refreshed {"socialAccountId":1499,"provider":"hubspot","state":"connected"} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"766570a3-89a2-4427-81c2-c70a7c780ee3","trace_id":"5c938a2d-aea9-4025-a7f7-733d8e8e9711"}
[2026-05-07 11:36:28] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2711
|
112
|
28
|
2026-05-07T11:36:40.591165+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153800591_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":4,"bounds":{"left":0.52859044,"top":0.0726257,"width":0.45977393,"height":0.9066241},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
291277805985683123
|
6378757184196315236
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2710
|
NULL
|
NULL
|
NULL
|
|
2713
|
112
|
29
|
2026-05-07T11:36:42.276314+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153802276_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4800532,"height":-0.06304872},"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-8970535408088379666
|
4648522587191274515
|
click
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2717
|
112
|
30
|
2026-05-07T11:36:47.577007+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153807577_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavigarecoderTavsco.s°9 masterProlede PhostormVIewINavigarecoderTavsco.s°9 masterProledey(C) [EMAIL]© Uuid.php> D Traits> D UseCases› DUser› DUtils› D Validation>Ovophp nelpers.onp© InitialFrontendState.phpc) Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial› D contrib• database, m docsfront-endlangnode_modules library root• O phpstanIM nublic> D resourcesDroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aoi.onopnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage> D debugbarM frameworkv Mloas.aitianoree audio wav= custom.loa720723727729=hubsnot-iournal-noll.log= laravel loal734 0 >740us tht is< $0 l6f Support Daily - in 24m100% L2Thu 7 May 14:36:47= custom.logE laravel.log X 4 SF (jiminny@localhost]A HS_Jocal [jiminny@localhost]# console [PKol)# console lcuy« console [STAGING]© RematchActivityOnCrmObjectDetach.phpDeleteCrmEntityTrait.phpAddkateLimitcommano.php© Hubspot/Client.php x © HandleRateLimit.php© SyncTeamMetadata.php©RateLimit.php© Configuration.php© RateLimitException.php©RateLimitinterface.php©) ProviderkateLimiter.phgclass Cllent extends Baseclient 1mpLements Hubspotclientintertace6826971* @return array<CrmFieldOption>public function fetch0pportunityFieldOptions(Field $field): arrayf…;* achnows badkequest* achrows hubspoctxcepc1onpublic function makeRequest(string $endpoint, Smethod = 'GET', $payload = [], ?string $queryString = null)Sendpoint = self::BASEURL . Sendpoint.1 Smethod === "GET') <wFelse {Sresponse = $this->getInstance()->getClient()->request($method, $endpoint, ["json'= (Snavload)ISremaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining');1 "309"Sinterval = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"$body=son decodel(scring) sresponse->gerbodyassociative: true);(Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('$max' . PHP_EOL . print_r($max, return: true));\Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('Sremaining' . PHP_EOL . print_r($remaining, return: true));uuLuminace supporc racades Loo..channel channel'custom_channel')->info('Sinterval ' . PHP_EOL . print_ r(Sinterval. return: true)):ILLuminate Support Facades Loq::channel( channel'custom_channel')->info('$body' . PHP_EOL . print_r($body, return: true));* athrows HubsnotExcentionnublic function createMeetina(arrav Spavload): Resnonse-...?...
|
NULL
|
-4939622420793281627
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavigarecoderTavsco.s°9 masterProlede PhostormVIewINavigarecoderTavsco.s°9 masterProledey(C) [EMAIL]© Uuid.php> D Traits> D UseCases› DUser› DUtils› D Validation>Ovophp nelpers.onp© InitialFrontendState.phpc) Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial› D contrib• database, m docsfront-endlangnode_modules library root• O phpstanIM nublic> D resourcesDroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aoi.onopnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage> D debugbarM frameworkv Mloas.aitianoree audio wav= custom.loa720723727729=hubsnot-iournal-noll.log= laravel loal734 0 >740us tht is< $0 l6f Support Daily - in 24m100% L2Thu 7 May 14:36:47= custom.logE laravel.log X 4 SF (jiminny@localhost]A HS_Jocal [jiminny@localhost]# console [PKol)# console lcuy« console [STAGING]© RematchActivityOnCrmObjectDetach.phpDeleteCrmEntityTrait.phpAddkateLimitcommano.php© Hubspot/Client.php x © HandleRateLimit.php© SyncTeamMetadata.php©RateLimit.php© Configuration.php© RateLimitException.php©RateLimitinterface.php©) ProviderkateLimiter.phgclass Cllent extends Baseclient 1mpLements Hubspotclientintertace6826971* @return array<CrmFieldOption>public function fetch0pportunityFieldOptions(Field $field): arrayf…;* achnows badkequest* achrows hubspoctxcepc1onpublic function makeRequest(string $endpoint, Smethod = 'GET', $payload = [], ?string $queryString = null)Sendpoint = self::BASEURL . Sendpoint.1 Smethod === "GET') <wFelse {Sresponse = $this->getInstance()->getClient()->request($method, $endpoint, ["json'= (Snavload)ISremaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining');1 "309"Sinterval = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"$body=son decodel(scring) sresponse->gerbodyassociative: true);(Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('$max' . PHP_EOL . print_r($max, return: true));\Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('Sremaining' . PHP_EOL . print_r($remaining, return: true));uuLuminace supporc racades Loo..channel channel'custom_channel')->info('Sinterval ' . PHP_EOL . print_ r(Sinterval. return: true)):ILLuminate Support Facades Loq::channel( channel'custom_channel')->info('$body' . PHP_EOL . print_r($body, return: true));* athrows HubsnotExcentionnublic function createMeetina(arrav Spavload): Resnonse-...?...
|
2713
|
NULL
|
NULL
|
NULL
|
|
2719
|
112
|
31
|
2026-05-07T11:36:49.845456+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153809845_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
15
Previous Highlighted Error
Next Highlighted Error
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ npx screenpipe@latest record --help
Start recording screen, audio, and serve the API
Usage: screenpipe record [OPTIONS]
Options:
-d, --audio-chunk-duration <AUDIO_CHUNK_DURATION>
Audio chunk duration in seconds
[default: 30]
-p, --port <PORT>
Port to run the server on
[default: 3030]
--disable-audio
Disable audio recording
-i, --audio-device <AUDIO_DEVICE>
Audio devices to use (can be specified multiple times)
--use-system-default-audio
Follow system default audio devices
--experimental-coreaudio-system-audio
[experimental, macOS 14.4+] Capture System Audio via CoreAudio Process Tap instead of ScreenCaptureKit. Off by default; ignored on older macOS and non-macOS
--data-dir <DATA_DIR>
Data directory. Default to $HOME/.screenpipe
--debug
Enable debug logging for screenpipe modules
-a, --audio-transcription-engine <AUDIO_TRANSCRIPTION_ENGINE>
Audio transcription engine to use
Possible values:
- deepgram
- whisper-tiny
- whisper-tiny-quantized
- whisper-large
- whisper-large-quantized
- whisper-large-v3-turbo
- whisper-large-v3-turbo-quantized
- openai-compatible
- qwen3-asr
- parakeet
- disabled: Disable transcription (audio capture only, no speech-to-text)
[default: parakeet]
-m, --monitor-id <MONITOR_ID>
Monitor IDs to record. May be specified multiple times. When set, only the listed monitors are recorded (implies `--use-all-monitors=false`)
--use-all-monitors
Automatically record all monitors. Ignored when `--monitor-id` is passed
-l, --language <LANGUAGE>
Languages for OCR/transcription
[possible values: english, chinese, german, spanish, russian, korean, french, japanese, portuguese, turkish, polish, catalan, dutch, arabic, swedish, italian, indonesian, hindi, vietnamese, finnish, hebrew, ukrainian, greek, malay, czech, romanian, danish, hungarian, norwegian, thai, urdu, croatian, bulgarian, lithuanian, latin, malayalam, welsh, slovak, persian, latvian, bengali, serbian, azerbaijani, slovenian, estonian, macedonian, nepali, mongolian, bosnian, kazakh, albanian, swahili, galician, marathi, punjabi, sinhala, khmer, afrikaans, belarusian, gujarati, amharic, yiddish, lao, uzbek, faroese, pashto, maltese, sanskrit, luxembourgish, myanmar, tibetan, tagalog, assamese, tatar, hausa, javanese]
--use-pii-removal
Enable PII removal
--filter-music
Filter music-dominant audio before transcription (reduces Spotify/YouTube music noise)
--disable-vision
Disable vision recording
--ignored-windows <IGNORED_WINDOWS>
Windows to ignore (by title, uses contains matching)
--included-windows <INCLUDED_WINDOWS>
Windows to include (by title, uses contains matching)
--ignored-urls <IGNORED_URLS>
URLs to ignore for browser privacy filtering
--deepgram-api-key <DEEPGRAM_API_KEY>
Deepgram API Key for audio transcription
--transcription-mode <TRANSCRIPTION_MODE>
Audio transcription scheduling mode: batch (default, longer chunks for quality) or realtime
Possible values:
- realtime: Transcribe immediately as audio is captured
- batch: Accumulate longer audio batches for better transcription quality (default)
[default: batch]
--disable-telemetry
Disable telemetry
--video-quality <VIDEO_QUALITY>
Video quality preset: low, balanced, high, max
[default: balanced]
--enable-sync
Enable cloud sync
--sync-token <SYNC_TOKEN>
API token for cloud sync
[env: SCREENPIPE_SYNC_TOKEN=]
--sync-password <SYNC_PASSWORD>
Password for encrypting synced data
[env: SCREENPIPE_SYNC_PASSWORD=[PASSWORD]
--sync-interval-secs <SYNC_INTERVAL_SECS>
Interval between sync cycles in seconds
[default: 300]
--sync-machine-id <SYNC_MACHINE_ID>
Override the machine ID for this device
--pause-on-drm-content
Pause screen and audio capture when a DRM-protected streaming app (Netflix, Disney+, etc.) or a remote-desktop client (Omnissa/VMware Horizon) is focused — these blank their windows while any app is recording the screen
--api-auth
Require authentication for remote API access. When enabled, non-localhost requests must include Authorization: Bearer <SCREENPIPE_API_KEY>. Localhost requests are always allowed
--listen-on-lan
Bind the HTTP server to [IP_ADDRESS] so other devices on the LAN can reach it. Off by default — the server binds [IP_ADDRESS] only. `--api-auth` is forced on whenever this flag is used; you can't accidentally expose an unauthenticated API on your network
--encrypt-secrets
Encrypt secrets (API keys, OAuth tokens) at rest using the OS keychain. Creates a keychain key if one doesn't exist. Without this flag, the CLI will use an existing key (created by the desktop app) but won't create one
--retention-days <RETENTION_DAYS>
Local data retention in days. Old screen/audio data is auto-deleted after this period. Set to 0 to disable retention (keep data forever)
[default: 14]
-h, --help
Print help (see a summary with '-h')
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"15","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":"lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ npx screenpipe@latest record --help\nStart recording screen, audio, and serve the API\n\nUsage: screenpipe record [OPTIONS]\n\nOptions:\n -d, --audio-chunk-duration <AUDIO_CHUNK_DURATION>\n Audio chunk duration in seconds\n\n [default: 30]\n\n -p, --port <PORT>\n Port to run the server on\n\n [default: 3030]\n\n --disable-audio\n Disable audio recording\n\n -i, --audio-device <AUDIO_DEVICE>\n Audio devices to use (can be specified multiple times)\n\n --use-system-default-audio\n Follow system default audio devices\n\n --experimental-coreaudio-system-audio\n [experimental, macOS 14.4+] Capture System Audio via CoreAudio Process Tap instead of ScreenCaptureKit. Off by default; ignored on older macOS and non-macOS\n\n --data-dir <DATA_DIR>\n Data directory. Default to $HOME/.screenpipe\n\n --debug\n Enable debug logging for screenpipe modules\n\n -a, --audio-transcription-engine <AUDIO_TRANSCRIPTION_ENGINE>\n Audio transcription engine to use\n\n Possible values:\n - deepgram\n - whisper-tiny\n - whisper-tiny-quantized\n - whisper-large\n - whisper-large-quantized\n - whisper-large-v3-turbo\n - whisper-large-v3-turbo-quantized\n - openai-compatible\n - qwen3-asr\n - parakeet\n - disabled: Disable transcription (audio capture only, no speech-to-text)\n\n [default: parakeet]\n\n -m, --monitor-id <MONITOR_ID>\n Monitor IDs to record. May be specified multiple times. When set, only the listed monitors are recorded (implies `--use-all-monitors=false`)\n\n --use-all-monitors\n Automatically record all monitors. Ignored when `--monitor-id` is passed\n\n -l, --language <LANGUAGE>\n Languages for OCR/transcription\n\n [possible values: english, chinese, german, spanish, russian, korean, french, japanese, portuguese, turkish, polish, catalan, dutch, arabic, swedish, italian, indonesian, hindi, vietnamese, finnish, hebrew, ukrainian, greek, malay, czech, romanian, danish, hungarian, norwegian, thai, urdu, croatian, bulgarian, lithuanian, latin, malayalam, welsh, slovak, persian, latvian, bengali, serbian, azerbaijani, slovenian, estonian, macedonian, nepali, mongolian, bosnian, kazakh, albanian, swahili, galician, marathi, punjabi, sinhala, khmer, afrikaans, belarusian, gujarati, amharic, yiddish, lao, uzbek, faroese, pashto, maltese, sanskrit, luxembourgish, myanmar, tibetan, tagalog, assamese, tatar, hausa, javanese]\n\n --use-pii-removal\n Enable PII removal\n\n --filter-music\n Filter music-dominant audio before transcription (reduces Spotify/YouTube music noise)\n\n --disable-vision\n Disable vision recording\n\n --ignored-windows <IGNORED_WINDOWS>\n Windows to ignore (by title, uses contains matching)\n\n --included-windows <INCLUDED_WINDOWS>\n Windows to include (by title, uses contains matching)\n\n --ignored-urls <IGNORED_URLS>\n URLs to ignore for browser privacy filtering\n\n --deepgram-api-key <DEEPGRAM_API_KEY>\n Deepgram API Key for audio transcription\n\n --transcription-mode <TRANSCRIPTION_MODE>\n Audio transcription scheduling mode: batch (default, longer chunks for quality) or realtime\n\n Possible values:\n - realtime: Transcribe immediately as audio is captured\n - batch: Accumulate longer audio batches for better transcription quality (default)\n\n [default: batch]\n\n --disable-telemetry\n Disable telemetry\n\n --video-quality <VIDEO_QUALITY>\n Video quality preset: low, balanced, high, max\n\n [default: balanced]\n\n --enable-sync\n Enable cloud sync\n\n --sync-token <SYNC_TOKEN>\n API token for cloud sync\n\n [env: SCREENPIPE_SYNC_TOKEN=]\n\n --sync-password <SYNC_PASSWORD>\n Password for encrypting synced data\n\n [env: SCREENPIPE_SYNC_PASSWORD=]\n\n --sync-interval-secs <SYNC_INTERVAL_SECS>\n Interval between sync cycles in seconds\n\n [default: 300]\n\n --sync-machine-id <SYNC_MACHINE_ID>\n Override the machine ID for this device\n\n --pause-on-drm-content\n Pause screen and audio capture when a DRM-protected streaming app (Netflix, Disney+, etc.) or a remote-desktop client (Omnissa/VMware Horizon) is focused — these blank their windows while any app is recording the screen\n\n --api-auth\n Require authentication for remote API access. When enabled, non-localhost requests must include Authorization: Bearer <SCREENPIPE_API_KEY>. Localhost requests are always allowed\n\n --listen-on-lan\n Bind the HTTP server to 0.0.0.0 so other devices on the LAN can reach it. Off by default — the server binds 127.0.0.1 only. `--api-auth` is forced on whenever this flag is used; you can't accidentally expose an unauthenticated API on your network\n\n --encrypt-secrets\n Encrypt secrets (API keys, OAuth tokens) at rest using the OS keychain. Creates a keychain key if one doesn't exist. Without this flag, the CLI will use an existing key (created by the desktop app) but won't create one\n\n --retention-days <RETENTION_DAYS>\n Local data retention in days. Old screen/audio data is auto-deleted after this period. Set to 0 to disable retention (keep data forever)\n\n [default: 14]\n\n -h, --help\n Print help (see a summary with '-h')","depth":4,"bounds":{"left":0.55019945,"top":0.0726257,"width":0.44980055,"height":0.9273743},"on_screen":true,"value":"lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ npx screenpipe@latest record --help\nStart recording screen, audio, and serve the API\n\nUsage: screenpipe record [OPTIONS]\n\nOptions:\n -d, --audio-chunk-duration <AUDIO_CHUNK_DURATION>\n Audio chunk duration in seconds\n\n [default: 30]\n\n -p, --port <PORT>\n Port to run the server on\n\n [default: 3030]\n\n --disable-audio\n Disable audio recording\n\n -i, --audio-device <AUDIO_DEVICE>\n Audio devices to use (can be specified multiple times)\n\n --use-system-default-audio\n Follow system default audio devices\n\n --experimental-coreaudio-system-audio\n [experimental, macOS 14.4+] Capture System Audio via CoreAudio Process Tap instead of ScreenCaptureKit. Off by default; ignored on older macOS and non-macOS\n\n --data-dir <DATA_DIR>\n Data directory. Default to $HOME/.screenpipe\n\n --debug\n Enable debug logging for screenpipe modules\n\n -a, --audio-transcription-engine <AUDIO_TRANSCRIPTION_ENGINE>\n Audio transcription engine to use\n\n Possible values:\n - deepgram\n - whisper-tiny\n - whisper-tiny-quantized\n - whisper-large\n - whisper-large-quantized\n - whisper-large-v3-turbo\n - whisper-large-v3-turbo-quantized\n - openai-compatible\n - qwen3-asr\n - parakeet\n - disabled: Disable transcription (audio capture only, no speech-to-text)\n\n [default: parakeet]\n\n -m, --monitor-id <MONITOR_ID>\n Monitor IDs to record. May be specified multiple times. When set, only the listed monitors are recorded (implies `--use-all-monitors=false`)\n\n --use-all-monitors\n Automatically record all monitors. Ignored when `--monitor-id` is passed\n\n -l, --language <LANGUAGE>\n Languages for OCR/transcription\n\n [possible values: english, chinese, german, spanish, russian, korean, french, japanese, portuguese, turkish, polish, catalan, dutch, arabic, swedish, italian, indonesian, hindi, vietnamese, finnish, hebrew, ukrainian, greek, malay, czech, romanian, danish, hungarian, norwegian, thai, urdu, croatian, bulgarian, lithuanian, latin, malayalam, welsh, slovak, persian, latvian, bengali, serbian, azerbaijani, slovenian, estonian, macedonian, nepali, mongolian, bosnian, kazakh, albanian, swahili, galician, marathi, punjabi, sinhala, khmer, afrikaans, belarusian, gujarati, amharic, yiddish, lao, uzbek, faroese, pashto, maltese, sanskrit, luxembourgish, myanmar, tibetan, tagalog, assamese, tatar, hausa, javanese]\n\n --use-pii-removal\n Enable PII removal\n\n --filter-music\n Filter music-dominant audio before transcription (reduces Spotify/YouTube music noise)\n\n --disable-vision\n Disable vision recording\n\n --ignored-windows <IGNORED_WINDOWS>\n Windows to ignore (by title, uses contains matching)\n\n --included-windows <INCLUDED_WINDOWS>\n Windows to include (by title, uses contains matching)\n\n --ignored-urls <IGNORED_URLS>\n URLs to ignore for browser privacy filtering\n\n --deepgram-api-key <DEEPGRAM_API_KEY>\n Deepgram API Key for audio transcription\n\n --transcription-mode <TRANSCRIPTION_MODE>\n Audio transcription scheduling mode: batch (default, longer chunks for quality) or realtime\n\n Possible values:\n - realtime: Transcribe immediately as audio is captured\n - batch: Accumulate longer audio batches for better transcription quality (default)\n\n [default: batch]\n\n --disable-telemetry\n Disable telemetry\n\n --video-quality <VIDEO_QUALITY>\n Video quality preset: low, balanced, high, max\n\n [default: balanced]\n\n --enable-sync\n Enable cloud sync\n\n --sync-token <SYNC_TOKEN>\n API token for cloud sync\n\n [env: SCREENPIPE_SYNC_TOKEN=]\n\n --sync-password <SYNC_PASSWORD>\n Password for encrypting synced data\n\n [env: SCREENPIPE_SYNC_PASSWORD=]\n\n --sync-interval-secs <SYNC_INTERVAL_SECS>\n Interval between sync cycles in seconds\n\n [default: 300]\n\n --sync-machine-id <SYNC_MACHINE_ID>\n Override the machine ID for this device\n\n --pause-on-drm-content\n Pause screen and audio capture when a DRM-protected streaming app (Netflix, Disney+, etc.) or a remote-desktop client (Omnissa/VMware Horizon) is focused — these blank their windows while any app is recording the screen\n\n --api-auth\n Require authentication for remote API access. When enabled, non-localhost requests must include Authorization: Bearer <SCREENPIPE_API_KEY>. Localhost requests are always allowed\n\n --listen-on-lan\n Bind the HTTP server to 0.0.0.0 so other devices on the LAN can reach it. Off by default — the server binds 127.0.0.1 only. `--api-auth` is forced on whenever this flag is used; you can't accidentally expose an unauthenticated API on your network\n\n --encrypt-secrets\n Encrypt secrets (API keys, OAuth tokens) at rest using the OS keychain. Creates a keychain key if one doesn't exist. Without this flag, the CLI will use an existing key (created by the desktop app) but won't create one\n\n --retention-days <RETENTION_DAYS>\n Local data retention in days. Old screen/audio data is auto-deleted after this period. Set to 0 to disable retention (keep data forever)\n\n [default: 14]\n\n -h, --help\n Print help (see a summary with '-h')","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3216904061287467739
|
7531555552090524004
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
15
Previous Highlighted Error
Next Highlighted Error
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/.screenpipe/data/data $ npx screenpipe@latest record --help
Start recording screen, audio, and serve the API
Usage: screenpipe record [OPTIONS]
Options:
-d, --audio-chunk-duration <AUDIO_CHUNK_DURATION>
Audio chunk duration in seconds
[default: 30]
-p, --port <PORT>
Port to run the server on
[default: 3030]
--disable-audio
Disable audio recording
-i, --audio-device <AUDIO_DEVICE>
Audio devices to use (can be specified multiple times)
--use-system-default-audio
Follow system default audio devices
--experimental-coreaudio-system-audio
[experimental, macOS 14.4+] Capture System Audio via CoreAudio Process Tap instead of ScreenCaptureKit. Off by default; ignored on older macOS and non-macOS
--data-dir <DATA_DIR>
Data directory. Default to $HOME/.screenpipe
--debug
Enable debug logging for screenpipe modules
-a, --audio-transcription-engine <AUDIO_TRANSCRIPTION_ENGINE>
Audio transcription engine to use
Possible values:
- deepgram
- whisper-tiny
- whisper-tiny-quantized
- whisper-large
- whisper-large-quantized
- whisper-large-v3-turbo
- whisper-large-v3-turbo-quantized
- openai-compatible
- qwen3-asr
- parakeet
- disabled: Disable transcription (audio capture only, no speech-to-text)
[default: parakeet]
-m, --monitor-id <MONITOR_ID>
Monitor IDs to record. May be specified multiple times. When set, only the listed monitors are recorded (implies `--use-all-monitors=false`)
--use-all-monitors
Automatically record all monitors. Ignored when `--monitor-id` is passed
-l, --language <LANGUAGE>
Languages for OCR/transcription
[possible values: english, chinese, german, spanish, russian, korean, french, japanese, portuguese, turkish, polish, catalan, dutch, arabic, swedish, italian, indonesian, hindi, vietnamese, finnish, hebrew, ukrainian, greek, malay, czech, romanian, danish, hungarian, norwegian, thai, urdu, croatian, bulgarian, lithuanian, latin, malayalam, welsh, slovak, persian, latvian, bengali, serbian, azerbaijani, slovenian, estonian, macedonian, nepali, mongolian, bosnian, kazakh, albanian, swahili, galician, marathi, punjabi, sinhala, khmer, afrikaans, belarusian, gujarati, amharic, yiddish, lao, uzbek, faroese, pashto, maltese, sanskrit, luxembourgish, myanmar, tibetan, tagalog, assamese, tatar, hausa, javanese]
--use-pii-removal
Enable PII removal
--filter-music
Filter music-dominant audio before transcription (reduces Spotify/YouTube music noise)
--disable-vision
Disable vision recording
--ignored-windows <IGNORED_WINDOWS>
Windows to ignore (by title, uses contains matching)
--included-windows <INCLUDED_WINDOWS>
Windows to include (by title, uses contains matching)
--ignored-urls <IGNORED_URLS>
URLs to ignore for browser privacy filtering
--deepgram-api-key <DEEPGRAM_API_KEY>
Deepgram API Key for audio transcription
--transcription-mode <TRANSCRIPTION_MODE>
Audio transcription scheduling mode: batch (default, longer chunks for quality) or realtime
Possible values:
- realtime: Transcribe immediately as audio is captured
- batch: Accumulate longer audio batches for better transcription quality (default)
[default: batch]
--disable-telemetry
Disable telemetry
--video-quality <VIDEO_QUALITY>
Video quality preset: low, balanced, high, max
[default: balanced]
--enable-sync
Enable cloud sync
--sync-token <SYNC_TOKEN>
API token for cloud sync
[env: SCREENPIPE_SYNC_TOKEN=]
--sync-password <SYNC_PASSWORD>
Password for encrypting synced data
[env: SCREENPIPE_SYNC_PASSWORD=[PASSWORD]
--sync-interval-secs <SYNC_INTERVAL_SECS>
Interval between sync cycles in seconds
[default: 300]
--sync-machine-id <SYNC_MACHINE_ID>
Override the machine ID for this device
--pause-on-drm-content
Pause screen and audio capture when a DRM-protected streaming app (Netflix, Disney+, etc.) or a remote-desktop client (Omnissa/VMware Horizon) is focused — these blank their windows while any app is recording the screen
--api-auth
Require authentication for remote API access. When enabled, non-localhost requests must include Authorization: Bearer <SCREENPIPE_API_KEY>. Localhost requests are always allowed
--listen-on-lan
Bind the HTTP server to [IP_ADDRESS] so other devices on the LAN can reach it. Off by default — the server binds [IP_ADDRESS] only. `--api-auth` is forced on whenever this flag is used; you can't accidentally expose an unauthenticated API on your network
--encrypt-secrets
Encrypt secrets (API keys, OAuth tokens) at rest using the OS keychain. Creates a keychain key if one doesn't exist. Without this flag, the CLI will use an existing key (created by the desktop app) but won't create one
--retention-days <RETENTION_DAYS>
Local data retention in days. Old screen/audio data is auto-deleted after this period. Set to 0 to disable retention (keep data forever)
[default: 14]
-h, --help
Print help (see a summary with '-h')
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2720
|
112
|
32
|
2026-05-07T11:36:55.402825+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153815402_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.4800532,"height":-0.06304872},"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-2034367797190539635
|
4792628979174108435
|
click
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
2719
|
NULL
|
NULL
|
NULL
|
|
2723
|
112
|
33
|
2026-05-07T11:37:25.915934+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153845915_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.27027926,"top":0.67517954,"width":0.4800532,"height":0.32482046},"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
3360244759534273448
|
1549467769159461127
|
idle
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2725
|
112
|
34
|
2026-05-07T11:37:38.838339+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153858838_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2723
|
NULL
|
NULL
|
NULL
|
|
2728
|
112
|
35
|
2026-05-07T11:37:47.010747+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153867010_m2.jpg...
|
Slack
|
Ves (DM) - Jiminny Inc - 3 new items - Slack
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stoyan Tanev
Stefka Stoyanova
Ves
Galya Dimitrova
Aneliya Angelova
Vasil Vasilev
James Graham
Nikolay Ivanov
Lukas Kovalik
you
Jira Cloud
Toast
Messages
Messages
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Ves
Apr 15th at 12:59:59 PM
12:59 PM
Здрасти
Искам да преместя някои listeners от default опашката
Jiminny\Listeners\Activities\Crm\AutoLogActivity
Jiminny\Listeners\Activities\Crm\AutoSyncActivity
мога ли да преместя тези съответно в crm-update и crm-sync
Lukas Kovalik
Apr 15th at 1:01:53 PM
1:01 PM
здрасти, да може
Apr 15th at 1:03:21 PM
1:03
crm-update може би
Jump to date
Lukas Kovalik
Apr 28th at 4:55:21 PM
4:55 PM
здрасти Вес, ако имаш момент да те питам някой неща
Ves
Apr 28th at 5:10:46 PM
5:10 PM
Здрасти
Lukas Kovalik
Apr 28th at 5:36:08 PM
5:36 PM
исках да те питам credentials на AWS. Направих server на постмарк за QAI но не знам как да добавя key, май вече не е през env. Има ли някакви инструкции?
12 replies
Last reply 9 days ago
View thread
Lukas Kovalik
Apr 28th at 5:44:08 PM
5:44 PM
И друго изглежда че има едни flavicon който изглежда по-различно на app и на pdf
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Apr 28th at 5:45:30 PM
5:45
забелязах че май идва от S3
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Apr 28th at 5:46:04 PM
5:46
[URL_WITH_CREDENTIALS] cat .env | grep POST
[ENV_SECRET]
Apr 28th at 6:45:40 PM
6:45
ето и стойността в момента на QAi
Lukas Kovalik
Apr 28th at 6:46:38 PM
Apr 28th at 6:46 PM
ами не знам тогава, не работи на QAI postmark и видях че няма добавен server
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Forward message…
Save for later
More actions
Apr 28th at 6:47:18 PM
6:47
добавих го, но може би беше направено през staging, ще го видя още веднъж
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Forward message…
Save for later
More actions
Ves
Apr 28th at 6:48:41 PM
Apr 28th at 6:48 PM
видя ли в Circle env?
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Forward message…
Save for later
More actions
Apr 28th at 6:50:17 PM
6:50
image.png
Channel product_launches...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.0056515955,"top":0.058260176,"width":0.011968086,"height":0.028731046},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.0029920214,"top":0.10055866,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.0066489363,"top":0.13806863,"width":0.009973404,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.0029920214,"top":0.15482841,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.0076462766,"top":0.19233839,"width":0.007978723,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.0029920214,"top":0.20909816,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.004986702,"top":0.24660814,"width":0.012965426,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.005319149,"top":0.24660814,"width":0.0026595744,"height":0.011173184}},{"char_start":1,"char_count":7,"bounds":{"left":0.0076462766,"top":0.24660814,"width":0.010638298,"height":0.011173184}}],"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.0029920214,"top":0.26336792,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.0076462766,"top":0.3008779,"width":0.0076462766,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.007978723,"top":0.3008779,"width":0.0019946808,"height":0.011173184}},{"char_start":1,"char_count":4,"bounds":{"left":0.009973404,"top":0.3008779,"width":0.0056515955,"height":0.011173184}}],"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.0029920214,"top":0.31763768,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.008643617,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.0019946808,"height":0.011173184}},{"char_start":1,"char_count":4,"bounds":{"left":0.00930851,"top":0.35514766,"width":0.0066489363,"height":0.011173184}}],"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.0029920214,"top":0.3719074,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.006981383,"top":0.4094174,"width":0.008976064,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.00731383,"top":0.4094174,"width":0.0033244682,"height":0.011173184}},{"char_start":1,"char_count":3,"bounds":{"left":0.010638298,"top":0.4094174,"width":0.0056515955,"height":0.011173184}}],"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.042220745,"top":0.11971269,"width":0.043882977,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.11971269,"width":0.0013297872,"height":0.014365523}},{"char_start":1,"char_count":24,"bounds":{"left":0.043550532,"top":0.11971269,"width":0.05418883,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.042220745,"top":0.14205906,"width":0.044215426,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.14205906,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":18,"bounds":{"left":0.045212764,"top":0.14205906,"width":0.04155585,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.042220745,"top":0.19473264,"width":0.022273935,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.19473264,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.04488032,"top":0.19473264,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.042220745,"top":0.21707901,"width":0.011968086,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.21707901,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.04488032,"top":0.21707901,"width":0.00930851,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.042220745,"top":0.23942538,"width":0.018284574,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.23942538,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.045212764,"top":0.23942538,"width":0.015292553,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.042220745,"top":0.26177174,"width":0.010305851,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.26177174,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":3,"bounds":{"left":0.045212764,"top":0.26177174,"width":0.00731383,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.042220745,"top":0.28411812,"width":0.034242023,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.28411812,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.04454787,"top":0.28411812,"width":0.032247342,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.042220745,"top":0.3064645,"width":0.027593086,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.3064645,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":12,"bounds":{"left":0.04454787,"top":0.3064645,"width":0.025265958,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.042220745,"top":0.32881084,"width":0.025598405,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.32881084,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":10,"bounds":{"left":0.04488032,"top":0.32881084,"width":0.022938829,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.042220745,"top":0.35115722,"width":0.015957447,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.35115722,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.04488032,"top":0.35115722,"width":0.013297873,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.042220745,"top":0.3735036,"width":0.022938829,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.3735036,"width":0.0013297872,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.043550532,"top":0.3735036,"width":0.021609042,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.042220745,"top":0.39584997,"width":0.034906916,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.39584997,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.045212764,"top":0.39584997,"width":0.031914894,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":24,"bounds":{"left":0.042220745,"top":0.41819632,"width":0.039893616,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.41819632,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.045212764,"top":0.41819632,"width":0.036901597,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.042220745,"top":0.4405427,"width":0.01662234,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.4405427,"width":0.0019946808,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.044215426,"top":0.4405427,"width":0.014960106,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.042220745,"top":0.46288908,"width":0.01761968,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.46288908,"width":0.0016622341,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.043882977,"top":0.46288908,"width":0.015957447,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.042220745,"top":0.48523542,"width":0.024268618,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.48523542,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":11,"bounds":{"left":0.04454787,"top":0.48523542,"width":0.021941489,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.042220745,"top":0.50758183,"width":0.016954787,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.50758183,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.04454787,"top":0.50758183,"width":0.01462766,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.042220745,"top":0.52992815,"width":0.024268618,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.52992815,"width":0.0019946808,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.044215426,"top":0.52992815,"width":0.022606382,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.042220745,"top":0.5522745,"width":0.04488032,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.5522745,"width":0.0019946808,"height":0.014365523}},{"char_start":1,"char_count":20,"bounds":{"left":0.044215426,"top":0.5522745,"width":0.04720745,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.6049481,"width":0.03756649,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.6049481,"width":0.0033244682,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.045545213,"top":0.6049481,"width":0.034242023,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.07945479,"top":0.6049481,"width":0.0063164895,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.08211436,"top":0.6049481,"width":0.014295213,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.08211436,"top":0.6049481,"width":0.0039893617,"height":0.014365523}},{"char_start":1,"char_count":13,"bounds":{"left":0.08610372,"top":0.6049481,"width":0.028922873,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.09607713,"top":0.62250596,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.09607713,"top":0.62250596,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11735372,"top":0.6049481,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":16,"bounds":{"left":0.1200133,"top":0.6049481,"width":0.03557181,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.042220745,"top":0.6272945,"width":0.028922873,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.6272945,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":11,"bounds":{"left":0.04488032,"top":0.6272945,"width":0.026263298,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.042220745,"top":0.64964086,"width":0.03756649,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.64964086,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.04488032,"top":0.64964086,"width":0.03523936,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Ves","depth":23,"bounds":{"left":0.042220745,"top":0.67198724,"width":0.0076462766,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.67198724,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":2,"bounds":{"left":0.045212764,"top":0.67198724,"width":0.004986702,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.042220745,"top":0.6943336,"width":0.034906916,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.6943336,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":14,"bounds":{"left":0.045877658,"top":0.6943336,"width":0.03158245,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.03756649,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.026263298,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.031914894,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.031914894,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.02925532,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"bounds":{"left":0.07413564,"top":0.7086991,"width":0.0063164895,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.021609042,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.011635638,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":18,"bounds":{"left":0.10206117,"top":0.09177973,"width":0.030585106,"height":0.030327214},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":20,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.01861702,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.0039893617,"height":0.012769354}},{"char_start":1,"char_count":7,"bounds":{"left":0.115359046,"top":0.10055866,"width":0.014960106,"height":0.012769354}}],"role_description":"text"},{"role":"AXRadioButton","text":"More","depth":19,"bounds":{"left":0.13397606,"top":0.09177973,"width":0.020279255,"height":0.030327214},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":18,"bounds":{"left":0.15392287,"top":0.09177973,"width":0.010970744,"height":0.030327214},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.015625,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.0076462766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":18,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Ves","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 15th at 12:59:59 PM","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"12:59 PM","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Здрасти","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Искам да преместя някои listeners от default опашката","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Jiminny\\Listeners\\Activities\\Crm\\AutoLogActivity\nJiminny\\Listeners\\Activities\\Crm\\AutoSyncActivity","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"мога ли да преместя тези съответно в crm-update и crm-sync","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 15th at 1:01:53 PM","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1:01 PM","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"здрасти, да може","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 15th at 1:03:21 PM","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1:03","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"crm-update може би","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":23,"bounds":{"left":0.12134308,"top":0.12689546,"width":0.050531916,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 4:55:21 PM","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4:55 PM","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"здрасти Вес, ако имаш момент да те питам някой неща","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Ves","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 5:10:46 PM","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:10 PM","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Здрасти","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.11801862,"top":0.11572227,"width":0.030917553,"height":0.007980846},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14860372,"top":0.11572227,"width":0.0029920214,"height":0.006384677},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 5:36:08 PM","depth":24,"bounds":{"left":0.1512633,"top":0.11572227,"width":0.015292553,"height":0.005586592},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:36 PM","depth":25,"bounds":{"left":0.1512633,"top":0.11572227,"width":0.015292553,"height":0.005586592},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"исках да те питам credentials на AWS. Направих server на постмарк за QAI но не знам как да добавя key, май вече не е през env. Има ли някакви инструкции?","depth":25,"bounds":{"left":0.11801862,"top":0.12529927,"width":0.07014628,"height":0.10215483},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11801862,"top":0.12529927,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":152,"bounds":{"left":0.11801862,"top":0.12529927,"width":0.07014628,"height":0.10215483}}],"role_description":"text"},{"role":"AXButton","text":"12 replies","depth":24,"bounds":{"left":0.13763298,"top":0.23463687,"width":0.01861702,"height":0.015961692},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Last reply 9 days ago","depth":25,"bounds":{"left":0.15890957,"top":0.23543495,"width":0.022606382,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15890957,"top":0.23623304,"width":0.0023271276,"height":0.012769354}},{"char_start":1,"char_count":20,"bounds":{"left":0.1612367,"top":0.23623304,"width":0.03723404,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"View thread","depth":25,"bounds":{"left":0.15890957,"top":0.23543495,"width":0.022938829,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15890957,"top":0.23623304,"width":0.0033244682,"height":0.012769354}},{"char_start":1,"char_count":10,"bounds":{"left":0.1619016,"top":0.23623304,"width":0.020279255,"height":0.012769354}}],"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.11801862,"top":0.26256984,"width":0.030917553,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.14860372,"top":0.264166,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 5:44:08 PM","depth":24,"bounds":{"left":0.1512633,"top":0.26656026,"width":0.015292553,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:44 PM","depth":25,"bounds":{"left":0.1512633,"top":0.26656026,"width":0.015292553,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15159574,"top":0.26656026,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":6,"bounds":{"left":0.15392287,"top":0.26656026,"width":0.012965426,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"И друго изглежда че има едни flavicon който изглежда по-различно на app и на pdf","depth":25,"bounds":{"left":0.11801862,"top":0.28172386,"width":0.07081117,"height":0.049481247},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11801862,"top":0.28172386,"width":0.0039893617,"height":0.014365523}},{"char_start":1,"char_count":79,"bounds":{"left":0.11801862,"top":0.28172386,"width":0.07081117,"height":0.049481247}}],"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.10472074,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.115359046,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.12599733,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.13663563,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.14727394,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.15791224,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.16855054,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.17918883,"top":0.2490024,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 28th at 5:45:30 PM","depth":25,"bounds":{"left":0.107380316,"top":0.34317636,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:45","depth":26,"bounds":{"left":0.107380316,"top":0.34317636,"width":0.007978723,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.107380316,"top":0.34317636,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":3,"bounds":{"left":0.109707445,"top":0.34317636,"width":0.005984043,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"забелязах че май идва от S3","depth":25,"bounds":{"left":0.11801862,"top":0.34078214,"width":0.065159574,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11801862,"top":0.34078214,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":26,"bounds":{"left":0.12034574,"top":0.34078214,"width":0.062832445,"height":0.014365523}}],"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.10472074,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.115359046,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.12599733,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.13663563,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.14727394,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.15791224,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.16855054,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.17918883,"top":0.3160415,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 28th at 5:46:04 PM","depth":25,"bounds":{"left":0.107380316,"top":0.36711892,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:46","depth":26,"bounds":{"left":0.107380316,"top":0.36711892,"width":0.007978723,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.107380316,"top":0.36711892,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":3,"bounds":{"left":0.109707445,"top":0.36711892,"width":0.005984043,"height":0.011971269}}],"role_description":"text"},{"role":"AXLink","text":"https://us-east-2.console.aws.amazon.com/s3/object/stage.ext.jiminny.public?region=us-east-2&prefix=favicon.ico","depth":25,"bounds":{"left":0.11801862,"top":0.36472467,"width":0.07081117,"height":0.08459697},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://us-east-2.console.aws.amazon.com/s3/object/stage.ext.jiminny.public?region=us-east-2&prefix=favicon.ico","depth":26,"bounds":{"left":0.11801862,"top":0.36472467,"width":0.07081117,"height":0.08459697},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11801862,"top":0.36472467,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":110,"bounds":{"left":0.11801862,"top":0.36472467,"width":0.07114362,"height":0.08459697}}],"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.10472074,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.115359046,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.12599733,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.13663563,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.14727394,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.15791224,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.16855054,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.17918883,"top":0.33998403,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 28th at 5:47:19 PM","depth":25,"bounds":{"left":0.107380316,"top":0.4612929,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"5:47","depth":26,"bounds":{"left":0.107380316,"top":0.4612929,"width":0.007978723,"height":0.011173184},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.107380316,"top":0.4612929,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":3,"bounds":{"left":0.109707445,"top":0.4612929,"width":0.005984043,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"може ли да го сменя, и съответно и на прод ако се окаже това","depth":25,"bounds":{"left":0.11801862,"top":0.45889863,"width":0.0625,"height":0.049481247},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.10472074,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.115359046,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.12599733,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.13663563,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.14727394,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.15791224,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.16855054,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.17918883,"top":0.43415803,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Ves","depth":24,"bounds":{"left":0.11801862,"top":0.51636076,"width":0.007978723,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.12599733,"top":0.5179569,"width":0.0026595744,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:36:29 PM","depth":24,"bounds":{"left":0.12832446,"top":0.5203512,"width":0.015292553,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:36 PM","depth":25,"bounds":{"left":0.12832446,"top":0.5203512,"width":0.015292553,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"виж че има .ext. в името, това не е ли от extension app","depth":25,"bounds":{"left":0.11801862,"top":0.5355148,"width":0.06815159,"height":0.031923383},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.10472074,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.115359046,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.12599733,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.13663563,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.14727394,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.15791224,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.16855054,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.17918883,"top":0.5027933,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 28th at 6:36:33 PM","depth":25,"bounds":{"left":0.107380316,"top":0.5794094,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:36","depth":26,"bounds":{"left":0.107380316,"top":0.5794094,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"https://github.com/jiminny/extension-app/tree/master/public","depth":25,"bounds":{"left":0.11801862,"top":0.57701516,"width":0.069148935,"height":0.031923383},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://github.com/jiminny/extension-app/tree/master/public","depth":26,"bounds":{"left":0.11801862,"top":0.57701516,"width":0.069148935,"height":0.031923383},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"bounds":{"left":0.10472074,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"bounds":{"left":0.115359046,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"bounds":{"left":0.12599733,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"bounds":{"left":0.13663563,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":26,"bounds":{"left":0.14727394,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"bounds":{"left":0.15791224,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"bounds":{"left":0.16855054,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"bounds":{"left":0.17918883,"top":0.5522745,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":24,"bounds":{"left":0.10372341,"top":0.6272945,"width":0.08577128,"height":0.030327214},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 5:36:08 PM","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apr 28th at 5:36 PM","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"исках да те питам credentials на AWS. Направих server на постмарк за QAI но не знам как да добавя key, май вече не е през env. Има ли някакви инструкции?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"12 replies","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Ves","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:33:30 PM","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apr 28th at 6:33 PM","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"в circle ci има такъв ключ","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:33:31 PM","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:33","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://app.circleci.com/settings/project/github/jiminny/app/environment-variables","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://app.circleci.com/settings/project/github/jiminny/app/environment-variables","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:33:36 PM","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:33","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POSTMARK_API_KEY_QAI","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Ves","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:45:32 PM","depth":24,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apr 28th at 6:45 PM","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"root@eb492432c515:/home/jiminny# cat .env | grep POST\nPOSTMARK_TOKEN=bd4151f9-3df1-4917-9458-5cd92b7dbe2b","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:45:40 PM","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:45","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ето и стойността в момента на QAi","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Lukas Kovalik","depth":24,"bounds":{"left":0.21675532,"top":0.0981644,"width":0.030917553,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.24734043,"top":0.09976058,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:46:38 PM","depth":24,"bounds":{"left":0.25,"top":0.10215483,"width":0.03656915,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apr 28th at 6:46 PM","depth":25,"bounds":{"left":0.25,"top":0.10215483,"width":0.03656915,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ами не знам тогава, не работи на QAI postmark и видях че няма добавен server","depth":25,"bounds":{"left":0.21675532,"top":0.11731844,"width":0.18085106,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 28th at 6:47:18 PM","depth":25,"bounds":{"left":0.20611702,"top":0.14365523,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:47","depth":26,"bounds":{"left":0.20611702,"top":0.14365523,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"добавих го, но може би беше направено през staging, ще го видя още веднъж","depth":25,"bounds":{"left":0.21675532,"top":0.14126097,"width":0.17952128,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Ves","depth":24,"bounds":{"left":0.21675532,"top":0.16360734,"width":0.007978723,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.22473404,"top":0.16520351,"width":0.0026595744,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Apr 28th at 6:48:41 PM","depth":24,"bounds":{"left":0.22706117,"top":0.16759777,"width":0.036236703,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apr 28th at 6:48 PM","depth":25,"bounds":{"left":0.22706117,"top":0.16759777,"width":0.036236703,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"видя ли в Circle env?","depth":25,"bounds":{"left":0.21675532,"top":0.18276137,"width":0.046875,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.2632979,"top":0.18435754,"width":0.0013297872,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(edited)","depth":25,"bounds":{"left":0.26462767,"top":0.18435754,"width":0.014295213,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"bounds":{"left":0.2789229,"top":0.18435754,"width":0.0013297872,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"React with white_check_mark","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":26,"on_screen":false,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":26,"on_screen":false,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Apr 28th at 6:50:17 PM","depth":25,"bounds":{"left":0.20611702,"top":0.20909816,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6:50","depth":26,"bounds":{"left":0.20611702,"top":0.20909816,"width":0.007978723,"height":0.011173184},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"image.png","depth":25,"bounds":{"left":0.21675532,"top":0.20670392,"width":0.019281914,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Channel product_launches","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.037898935,"height":0.0007980846},"on_screen":true,"role_description":"text"}]...
|
2720509602097012514
|
-8198497702746095494
|
click
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stoyan Tanev
Stefka Stoyanova
Ves
Galya Dimitrova
Aneliya Angelova
Vasil Vasilev
James Graham
Nikolay Ivanov
Lukas Kovalik
you
Jira Cloud
Toast
Messages
Messages
More
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Ves
Apr 15th at 12:59:59 PM
12:59 PM
Здрасти
Искам да преместя някои listeners от default опашката
Jiminny\Listeners\Activities\Crm\AutoLogActivity
Jiminny\Listeners\Activities\Crm\AutoSyncActivity
мога ли да преместя тези съответно в crm-update и crm-sync
Lukas Kovalik
Apr 15th at 1:01:53 PM
1:01 PM
здрасти, да може
Apr 15th at 1:03:21 PM
1:03
crm-update може би
Jump to date
Lukas Kovalik
Apr 28th at 4:55:21 PM
4:55 PM
здрасти Вес, ако имаш момент да те питам някой неща
Ves
Apr 28th at 5:10:46 PM
5:10 PM
Здрасти
Lukas Kovalik
Apr 28th at 5:36:08 PM
5:36 PM
исках да те питам credentials на AWS. Направих server на постмарк за QAI но не знам как да добавя key, май вече не е през env. Има ли някакви инструкции?
12 replies
Last reply 9 days ago
View thread
Lukas Kovalik
Apr 28th at 5:44:08 PM
5:44 PM
И друго изглежда че има едни flavicon който изглежда по-различно на app и на pdf
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Apr 28th at 5:45:30 PM
5:45
забелязах че май идва от S3
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Apr 28th at 5:46:04 PM
5:46
[URL_WITH_CREDENTIALS] cat .env | grep POST
[ENV_SECRET]
Apr 28th at 6:45:40 PM
6:45
ето и стойността в момента на QAi
Lukas Kovalik
Apr 28th at 6:46:38 PM
Apr 28th at 6:46 PM
ами не знам тогава, не работи на QAI postmark и видях че няма добавен server
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Forward message…
Save for later
More actions
Apr 28th at 6:47:18 PM
6:47
добавих го, но може би беше направено през staging, ще го видя още веднъж
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Forward message…
Save for later
More actions
Ves
Apr 28th at 6:48:41 PM
Apr 28th at 6:48 PM
видя ли в Circle env?
(edited)
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Forward message…
Save for later
More actions
Apr 28th at 6:50:17 PM
6:50
image.png
Channel product_launches
. 50 lilSupport Daily - in 23m100% C&Thu 7 May 14:37:46a Q tội= custom.log X = laravel.logA SF (jiminny@localhost]A HS_Jocal [jiminny@localhost]# console [PKol)A console (eu)« console [STAGING]=CrmEntityTrait.phpRateLimit.onpLukas Kovalik Aor 28th at 6:46 PMами не знам тогава, не работи на оді vostmаrk и вилях че няма лобавен servenдобавих го, но може ои оеше направено през staging, ще го видя още ведньжVes Apr 28th at 6:48 PMвидя ли в Circle env. (edited)ActivityLaterMoreslackVIewJiminny ...# Starred8 jiminny-x-integrati...& platform-inner-team© Channels# ai-chapter# alertsit backend# bugs# confusion-clinid# curiosity_lab# engineering# general#jiminny-bgi platform-nckets# product_launchesa randomi released# sofia-office# support# thank-yous# the_people_of jimi.... Direct messagesB Aneliya Angelova,...8o Stoyan Tanev •a. Stefka Stoyanova• Ves®. Galya Dimitrova>O scriptsvD storage> D debugbarM frameworkv Mloas.aitianoreD audio.wav= custom.loa=hubsnot-iournal-noll.log= laravel.logus tht isHistoryWindowHelpQ Describe what you are looking for&. VesThread• Messagesur Tuesday April 28th ~ antialsна Avvs. Manавих server напостмарк за чмі но не знамкак да добавя кеу, май вече неe nрез env. иіма ли някаквиинструкции.O a 12 replies Last reply...Lukas Kovallk 5-44 PMIИ друго изглежда че има едниflavicon който изглежда по-различно на арр и на pdfзабелязах че май идва от S32.console.aws.amazon.com/s3/object/stage.ext.jiminny.public?region=us-east-Anreny=tavicon icoможе ли да го сменя, исьответно и на поол ако сеокаже товаVes 6:36 PMвиж че има .ext. в името, товане е ли от extension [URL_WITH_CREDENTIALS] BadRequest* athrows HubsnotExcentionnublic function createMeetina(arrav Spavload): Resnonse-..?fo 4 spaces 0...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2729
|
112
|
36
|
2026-05-07T11:37:49.285182+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153869285_m2.jpg...
|
Slack
|
product_launches (Channel) - Jiminny Inc - 3 new i product_launches (Channel) - Jiminny Inc - 3 new items - Slack...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stoyan Tanev
Stefka Stoyanova
Ves
Galya Dimitrova
Aneliya Angelova
Vasil Vasilev
James Graham
Nikolay Ivanov
Lukas Kovalik
you
Jira Cloud
Toast
Messages
Messages
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Galya Dimitrova
Apr 30th at 3:36:41 PM
3:36 PM
Ask Jiminny Panorama Reports are now live
for all customers that have Panorama chat
For more details please refer to the previous slack post about the feature -
https://jiminny.slack.com/archives/C012P3QDF43/p1776944267826459
https://jiminny.slack.com/archives/C012P3QDF43/p1776944267826459
When the users goes to AI Reports they will see a landing page that gives them details about the feature and shows a video of how the feature works (see attached image).
Emails about the launch will be send to all Admins and Managers from the companies that use AJ Panorama.
Screenshot 2026-04-30 at 15.32.10.png
Toggle file
Screenshot 2026-04-30 at 15.32.10.png
Galya Dimitrova
Galya Dimitrova
Hey team,
Our latest product update is live. Here’s what you need to know:
Ask Jiminny Panorama reports are live [for Jiminny users only]
Why we did it
Managers use AJ Panorama to track what is happening with their team and how they are performing. They want to track this over time and currently they have to go every week and check it manually. We wanted to automate this process for them - send them the report they need straight into their inbox when they need it.
Show more
4 files
Toggle 4 files
Download all
Screenshot 2026-04-23 at 14.21.22.png Screenshot 2026-04-23 at 14.21.22.png PNG
Screenshot 2026-04-23 at 14.21.22.png
PNG
Screenshot 2026-04-23 at 14.21.03.png Screenshot 2026-04-23 at 14.21.03.png PNG
Screenshot 2026-04-23 at 14.21.03.png
PNG
Becky's Objection Handling Tracking - 16 - 22 Apr 2026.pdf PDF
Becky's Objection Handling Tracking - 16 - 22 Apr 2026.pdf
PDF
At-risk Account Sentiment Monitor - 16 - 22 Apr 2026.pdf PDF
At-risk Account Sentiment Monitor - 16 - 22 Apr 2026.pdf
PDF
Thread in product_launches
Thread in
product_launches
|
Apr 23rd
Apr 23rd
|
View message
View message
3 reactions, react with raised hands emoji
3
Add reaction…
Jump to date
New
Adelina Petrova
Today at 2:14:13 PM
2:14 PM
Hey team,
Our latest product update is live. Here’s what you need to know:
Leaderboard View for AI Call Scoring, Coaching Frameworks & Key Words Scoring
Why we did it
Some customers wanted a clearer way to identify top performers and compare team results more easily. Previously, scoring results were harder to scan quickly, especially for managers reviewing large teams.
How it works
• AI Call Scoring, Coaching Frameworks, and Keyword Scoring results now automatically display in leaderboard order
• Top-performing reps appear at the top based on score
• Managers can quickly identify high performers and coaching opportunities
This gives teams better visibility into performance trends and helps surface coaching insights more quickly
Screenshot 2026-05-07 at 12.54.14.png
Toggle file
Screenshot 2026-05-07 at 12.54.14.png
Download Screenshot 2026-05-07 at 12.54.14.png
Share file: Screenshot 2026-05-07 at 12.54.14.png
View canvas details
More actions
3 reactions, react with raised hands emoji
3
Add reaction…
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Channel product_launches...
|
[{"role":"AXPopUpButton","text [{"role":"AXPopUpButton","text":"Switch workspaces… (Jiminny Inc) Has new messages","depth":14,"bounds":{"left":0.0056515955,"top":0.058260176,"width":0.011968086,"height":0.028731046},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Home","depth":14,"bounds":{"left":0.0029920214,"top":0.10055866,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Home","depth":16,"bounds":{"left":0.0066489363,"top":0.13806863,"width":0.009973404,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DMs","depth":14,"bounds":{"left":0.0029920214,"top":0.15482841,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"DMs","depth":16,"bounds":{"left":0.0076462766,"top":0.19233839,"width":0.007978723,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Activity","depth":14,"bounds":{"left":0.0029920214,"top":0.20909816,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Activity","depth":16,"bounds":{"left":0.004986702,"top":0.24660814,"width":0.012965426,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.005319149,"top":0.24660814,"width":0.0026595744,"height":0.011173184}},{"char_start":1,"char_count":7,"bounds":{"left":0.0076462766,"top":0.24660814,"width":0.010638298,"height":0.011173184}}],"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":14,"bounds":{"left":0.0029920214,"top":0.26336792,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":16,"bounds":{"left":0.0076462766,"top":0.3008779,"width":0.0076462766,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.007978723,"top":0.3008779,"width":0.0019946808,"height":0.011173184}},{"char_start":1,"char_count":4,"bounds":{"left":0.009973404,"top":0.3008779,"width":0.0056515955,"height":0.011173184}}],"role_description":"text"},{"role":"AXRadioButton","text":"Later","depth":14,"bounds":{"left":0.0029920214,"top":0.31763768,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Later","depth":16,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.008643617,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.00731383,"top":0.35514766,"width":0.0019946808,"height":0.011173184}},{"char_start":1,"char_count":4,"bounds":{"left":0.00930851,"top":0.35514766,"width":0.0066489363,"height":0.011173184}}],"role_description":"text"},{"role":"AXRadioButton","text":"More…","depth":14,"bounds":{"left":0.0029920214,"top":0.3719074,"width":0.017287234,"height":0.054269753},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"More","depth":16,"bounds":{"left":0.006981383,"top":0.4094174,"width":0.008976064,"height":0.0103751},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.00731383,"top":0.4094174,"width":0.0033244682,"height":0.011173184}},{"char_start":1,"char_count":3,"bounds":{"left":0.010638298,"top":0.4094174,"width":0.0056515955,"height":0.011173184}}],"role_description":"text"},{"role":"AXStaticText","text":"Unreads","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Threads","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Huddles","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Drafts & sent","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Directories","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jiminny-x-integration-app","depth":23,"bounds":{"left":0.042220745,"top":0.11971269,"width":0.043882977,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.11971269,"width":0.0013297872,"height":0.014365523}},{"char_start":1,"char_count":24,"bounds":{"left":0.043550532,"top":0.11971269,"width":0.05418883,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"platform-inner-team","depth":23,"bounds":{"left":0.042220745,"top":0.14205906,"width":0.044215426,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.14205906,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":18,"bounds":{"left":0.045212764,"top":0.14205906,"width":0.04155585,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"ai-chapter","depth":23,"bounds":{"left":0.042220745,"top":0.19473264,"width":0.022273935,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.19473264,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.04488032,"top":0.19473264,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"alerts","depth":23,"bounds":{"left":0.042220745,"top":0.21707901,"width":0.011968086,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.21707901,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.04488032,"top":0.21707901,"width":0.00930851,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"backend","depth":23,"bounds":{"left":0.042220745,"top":0.23942538,"width":0.018284574,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.23942538,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.045212764,"top":0.23942538,"width":0.015292553,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"bugs","depth":23,"bounds":{"left":0.042220745,"top":0.26177174,"width":0.010305851,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.26177174,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":3,"bounds":{"left":0.045212764,"top":0.26177174,"width":0.00731383,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"confusion-clinic","depth":23,"bounds":{"left":0.042220745,"top":0.28411812,"width":0.034242023,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.28411812,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.04454787,"top":0.28411812,"width":0.032247342,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"curiosity_lab","depth":23,"bounds":{"left":0.042220745,"top":0.3064645,"width":0.027593086,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.3064645,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":12,"bounds":{"left":0.04454787,"top":0.3064645,"width":0.025265958,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"engineering","depth":23,"bounds":{"left":0.042220745,"top":0.32881084,"width":0.025598405,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.32881084,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":10,"bounds":{"left":0.04488032,"top":0.32881084,"width":0.022938829,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"general","depth":23,"bounds":{"left":0.042220745,"top":0.35115722,"width":0.015957447,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.35115722,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.04488032,"top":0.35115722,"width":0.013297873,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"jiminny-bg","depth":23,"bounds":{"left":0.042220745,"top":0.3735036,"width":0.022938829,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.3735036,"width":0.0013297872,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.043550532,"top":0.3735036,"width":0.021609042,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"platform-tickets","depth":23,"bounds":{"left":0.042220745,"top":0.39584997,"width":0.034906916,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.39584997,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.045212764,"top":0.39584997,"width":0.031914894,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":23,"bounds":{"left":0.042220745,"top":0.41819632,"width":0.03856383,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.41819632,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.045212764,"top":0.41819632,"width":0.03557181,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"random","depth":23,"bounds":{"left":0.042220745,"top":0.4405427,"width":0.01662234,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.4405427,"width":0.0019946808,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.044215426,"top":0.4405427,"width":0.014960106,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"releases","depth":23,"bounds":{"left":0.042220745,"top":0.46288908,"width":0.01761968,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.46288908,"width":0.0016622341,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.043882977,"top":0.46288908,"width":0.015957447,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"sofia-office","depth":23,"bounds":{"left":0.042220745,"top":0.48523542,"width":0.024268618,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.48523542,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":11,"bounds":{"left":0.04454787,"top":0.48523542,"width":0.021941489,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"support","depth":23,"bounds":{"left":0.042220745,"top":0.50758183,"width":0.016954787,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.50758183,"width":0.0023271276,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.04454787,"top":0.50758183,"width":0.01462766,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"thank-yous","depth":23,"bounds":{"left":0.042220745,"top":0.52992815,"width":0.024268618,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.52992815,"width":0.0019946808,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.044215426,"top":0.52992815,"width":0.022606382,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"the_people_of_jiminny","depth":23,"bounds":{"left":0.042220745,"top":0.5522745,"width":0.04488032,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.5522745,"width":0.0019946808,"height":0.014365523}},{"char_start":1,"char_count":20,"bounds":{"left":0.044215426,"top":0.5522745,"width":0.04720745,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.6049481,"width":0.03756649,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.6049481,"width":0.0033244682,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.045545213,"top":0.6049481,"width":0.034242023,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.07945479,"top":0.6049481,"width":0.0063164895,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Yankov","depth":23,"bounds":{"left":0.08211436,"top":0.6049481,"width":0.014295213,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.08211436,"top":0.6049481,"width":0.0039893617,"height":0.014365523}},{"char_start":1,"char_count":13,"bounds":{"left":0.08610372,"top":0.6049481,"width":0.028922873,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":",","depth":23,"bounds":{"left":0.09607713,"top":0.62250596,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Steliyan Georgiev","depth":23,"bounds":{"left":0.09607713,"top":0.62250596,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11735372,"top":0.6049481,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":16,"bounds":{"left":0.1200133,"top":0.6049481,"width":0.03557181,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Stoyan Tanev","depth":23,"bounds":{"left":0.042220745,"top":0.6272945,"width":0.028922873,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.6272945,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":11,"bounds":{"left":0.04488032,"top":0.6272945,"width":0.026263298,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Stefka Stoyanova","depth":23,"bounds":{"left":0.042220745,"top":0.64964086,"width":0.03756649,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.64964086,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":15,"bounds":{"left":0.04488032,"top":0.64964086,"width":0.03523936,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Ves","depth":23,"bounds":{"left":0.042220745,"top":0.67198724,"width":0.0076462766,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.67198724,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":2,"bounds":{"left":0.045212764,"top":0.67198724,"width":0.004986702,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Galya Dimitrova","depth":23,"bounds":{"left":0.042220745,"top":0.6943336,"width":0.034906916,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.042220745,"top":0.6943336,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":14,"bounds":{"left":0.045877658,"top":0.6943336,"width":0.03158245,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Aneliya Angelova","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.03756649,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Vasil Vasilev","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.026263298,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"James Graham","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.031914894,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Nikolay Ivanov","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.031914894,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.02925532,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"you","depth":23,"bounds":{"left":0.07413564,"top":0.7086991,"width":0.0063164895,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jira Cloud","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.021609042,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Toast","depth":23,"bounds":{"left":0.042220745,"top":0.7086991,"width":0.011635638,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Messages","depth":17,"bounds":{"left":0.10206117,"top":0.09177973,"width":0.030585106,"height":0.030327214},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true,"is_expanded":false},{"role":"AXStaticText","text":"Messages","depth":19,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.01861702,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.111369684,"top":0.10055866,"width":0.0039893617,"height":0.012769354}},{"char_start":1,"char_count":7,"bounds":{"left":0.115359046,"top":0.10055866,"width":0.014960106,"height":0.012769354}}],"role_description":"text"},{"role":"AXRadioButton","text":"Files","depth":17,"bounds":{"left":0.13397606,"top":0.09177973,"width":0.020944148,"height":0.030327214},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Files","depth":19,"bounds":{"left":0.14328457,"top":0.10055866,"width":0.008976064,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.14328457,"top":0.10055866,"width":0.0026595744,"height":0.012769354}},{"char_start":1,"char_count":4,"bounds":{"left":0.14594415,"top":0.10055866,"width":0.0063164895,"height":0.012769354}}],"role_description":"text"},{"role":"AXRadioButton","text":"Pins","depth":17,"bounds":{"left":0.15591756,"top":0.09177973,"width":0.020279255,"height":0.030327214},"on_screen":true,"role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Pins","depth":19,"bounds":{"left":0.16522606,"top":0.10055866,"width":0.00831117,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.16555852,"top":0.10055866,"width":0.0026595744,"height":0.012769354}},{"char_start":1,"char_count":3,"bounds":{"left":0.16821809,"top":0.10055866,"width":0.0056515955,"height":0.012769354}}],"role_description":"text"},{"role":"AXPopUpButton","text":"Add and Edit Channel Tabs","depth":17,"bounds":{"left":0.1775266,"top":0.09177973,"width":0.010638298,"height":0.030327214},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Canvas","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.015625,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"List","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.0076462766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Folder","depth":17,"bounds":{"left":0.096409574,"top":0.0518755,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"bounds":{"left":0.2357048,"top":0.12689546,"width":0.05285904,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Galya Dimitrova","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Apr 30th at 3:36:41 PM","depth":23,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3:36 PM","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Ask Jiminny Panorama Reports are now live","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"for all customers that have Panorama chat","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For more details please refer to the previous slack post about the feature -","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://jiminny.slack.com/archives/C012P3QDF43/p1776944267826459","depth":23,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://jiminny.slack.com/archives/C012P3QDF43/p1776944267826459","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"When the users goes to AI Reports they will see a landing page that gives them details about the feature and shows a video of how the feature works (see attached image).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Emails about the launch will be send to all Admins and Managers from the companies that use AJ Panorama.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Screenshot 2026-04-30 at 15.32.10.png","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Toggle file","depth":24,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXLink","text":"Screenshot 2026-04-30 at 15.32.10.png","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Galya Dimitrova","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Galya Dimitrova","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hey team,","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Our latest product update is live. Here’s what you need to know:","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Ask Jiminny Panorama reports are live [for Jiminny users only]","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Why we did it","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Managers use AJ Panorama to track what is happening with their team and how they are performing. They want to track this over time and currently they have to go every week and check it manually. We wanted to automate this process for them - send them the report they need straight into their inbox when they need it.","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4 files","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Toggle 4 files","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Download all","depth":26,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Screenshot 2026-04-23 at 14.21.22.png Screenshot 2026-04-23 at 14.21.22.png PNG","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Screenshot 2026-04-23 at 14.21.22.png","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PNG","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Screenshot 2026-04-23 at 14.21.03.png Screenshot 2026-04-23 at 14.21.03.png PNG","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Screenshot 2026-04-23 at 14.21.03.png","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PNG","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Becky's Objection Handling Tracking - 16 - 22 Apr 2026.pdf PDF","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Becky's Objection Handling Tracking - 16 - 22 Apr 2026.pdf","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PDF","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"At-risk Account Sentiment Monitor - 16 - 22 Apr 2026.pdf PDF","depth":25,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"At-risk Account Sentiment Monitor - 16 - 22 Apr 2026.pdf","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PDF","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"Thread in product_launches","depth":25,"bounds":{"left":0.12333777,"top":0.12051077,"width":0.05285904,"height":0.011971269},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Thread in","depth":26,"bounds":{"left":0.12333777,"top":0.12051077,"width":0.01761968,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.12333777,"top":0.121308856,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":8,"bounds":{"left":0.12566489,"top":0.121308856,"width":0.014295213,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"product_launches","depth":26,"bounds":{"left":0.14527926,"top":0.12051077,"width":0.030917553,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1456117,"top":0.121308856,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":15,"bounds":{"left":0.14760639,"top":0.121308856,"width":0.028922873,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"|","depth":25,"bounds":{"left":0.17619681,"top":0.12051077,"width":0.0029920214,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Apr 23rd","depth":25,"bounds":{"left":0.17918883,"top":0.12051077,"width":0.015625,"height":0.011971269},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Apr 23rd","depth":26,"bounds":{"left":0.17918883,"top":0.12051077,"width":0.015625,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.17918883,"top":0.121308856,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":7,"bounds":{"left":0.1818484,"top":0.121308856,"width":0.013297873,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"|","depth":25,"bounds":{"left":0.19448139,"top":0.12051077,"width":0.0033244682,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"View message","depth":25,"bounds":{"left":0.1974734,"top":0.12051077,"width":0.024933511,"height":0.011971269},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"View message","depth":26,"bounds":{"left":0.1974734,"top":0.12051077,"width":0.024933511,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.19780585,"top":0.121308856,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":11,"bounds":{"left":0.20046543,"top":0.121308856,"width":0.022273935,"height":0.011971269}}],"role_description":"text"},{"role":"AXCheckBox","text":"3 reactions, react with raised hands emoji","depth":24,"bounds":{"left":0.11801862,"top":0.14046289,"width":0.020944148,"height":0.019952115},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":25,"bounds":{"left":0.13397606,"top":0.14445332,"width":0.0023271276,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Add reaction…","depth":24,"bounds":{"left":0.13996011,"top":0.14046289,"width":0.011635638,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Jump to date","depth":22,"bounds":{"left":0.24933511,"top":0.17717478,"width":0.025598405,"height":0.023144454},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New","depth":22,"bounds":{"left":0.41223404,"top":0.1819633,"width":0.008976064,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Adelina Petrova","depth":23,"bounds":{"left":0.11801862,"top":0.20830008,"width":0.035904255,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"","depth":23,"bounds":{"left":0.16023937,"top":0.20989625,"width":0.0026595744,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXLink","text":"Today at 2:14:13 PM","depth":23,"bounds":{"left":0.16289894,"top":0.2122905,"width":0.015292553,"height":0.011971269},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2:14 PM","depth":24,"bounds":{"left":0.16289894,"top":0.2122905,"width":0.015292553,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.16289894,"top":0.21308859,"width":0.0026595744,"height":0.011971269}},{"char_start":1,"char_count":6,"bounds":{"left":0.16522606,"top":0.21308859,"width":0.012965426,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"Hey team,","depth":23,"bounds":{"left":0.11801862,"top":0.22745411,"width":0.022273935,"height":0.015163607},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11801862,"top":0.22825219,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":8,"bounds":{"left":0.12167553,"top":0.22825219,"width":0.01861702,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Our latest product update is live. Here’s what you need to know:","depth":23,"bounds":{"left":0.11801862,"top":0.24501197,"width":0.13996011,"height":0.015163607},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11801862,"top":0.24581006,"width":0.0039893617,"height":0.014365523}},{"char_start":1,"char_count":63,"bounds":{"left":0.12200798,"top":0.24581006,"width":0.13630319,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Leaderboard View for AI Call Scoring, Coaching Frameworks & Key Words Scoring","depth":23,"bounds":{"left":0.13364361,"top":0.26256984,"width":0.18085106,"height":0.015163607},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.13397606,"top":0.26336792,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":76,"bounds":{"left":0.13630319,"top":0.26336792,"width":0.17819148,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Why we did it","depth":23,"bounds":{"left":0.11801862,"top":0.28651237,"width":0.030585106,"height":0.015163607},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.11801862,"top":0.28731045,"width":0.005319149,"height":0.014365523}},{"char_start":1,"char_count":12,"bounds":{"left":0.12333777,"top":0.28731045,"width":0.025265958,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Some customers wanted a clearer way to identify top performers and compare team results more easily. Previously, scoring results were harder to scan quickly, especially for managers reviewing large teams.","depth":23,"bounds":{"left":0.11801862,"top":0.30407023,"width":0.29554522,"height":0.032721467},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"How it works","depth":23,"bounds":{"left":0.11801862,"top":0.34557062,"width":0.029587766,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"• AI Call Scoring, Coaching Frameworks, and Keyword Scoring results now automatically display in leaderboard order","depth":23,"bounds":{"left":0.11801862,"top":0.36312848,"width":0.2543218,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"• Top-performing reps appear at the top based on score","depth":23,"bounds":{"left":0.11801862,"top":0.38068634,"width":0.12134308,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"• Managers can quickly identify high performers and coaching opportunities","depth":23,"bounds":{"left":0.11801862,"top":0.3982442,"width":0.1662234,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"This gives teams better visibility into performance trends and helps surface coaching insights more quickly","depth":23,"bounds":{"left":0.11801862,"top":0.41580206,"width":0.23071809,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Screenshot 2026-05-07 at 12.54.14.png","depth":24,"bounds":{"left":0.11801862,"top":0.43735036,"width":0.07646277,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":24,"bounds":{"left":0.19448139,"top":0.4365523,"width":0.0013297872,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Toggle file","depth":24,"bounds":{"left":0.19581117,"top":0.43575418,"width":0.0066489363,"height":0.016759777},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXLink","text":"Screenshot 2026-05-07 at 12.54.14.png","depth":26,"bounds":{"left":0.11801862,"top":0.4557063,"width":0.11968085,"height":0.16919394},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Download Screenshot 2026-05-07 at 12.54.14.png","depth":27,"bounds":{"left":0.19049202,"top":0.4668795,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share file: Screenshot 2026-05-07 at 12.54.14.png","depth":27,"bounds":{"left":0.20113032,"top":0.4668795,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View canvas details","depth":27,"bounds":{"left":0.21176861,"top":0.4668795,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":27,"bounds":{"left":0.22240691,"top":0.4668795,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"3 reactions, react with raised hands emoji","depth":24,"bounds":{"left":0.11801862,"top":0.63048685,"width":0.027593086,"height":0.0023942539},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":25,"bounds":{"left":0.140625,"top":0.632083,"width":0.0023271276,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Add reaction…","depth":24,"bounds":{"left":0.14660904,"top":0.63048685,"width":0.011635638,"height":0.0023942539},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with white_check_mark","depth":25,"bounds":{"left":0.33543882,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with eyes","depth":25,"bounds":{"left":0.3460771,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"React with raised_hands","depth":25,"bounds":{"left":0.3567154,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Add reaction…","depth":25,"bounds":{"left":0.36735374,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reply in thread","depth":25,"bounds":{"left":0.37799203,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Forward message…","depth":25,"bounds":{"left":0.38863033,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Save for later","depth":25,"bounds":{"left":0.39926863,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"toggle button","subrole":"AXToggleButton","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More actions","depth":25,"bounds":{"left":0.40990692,"top":0.19553073,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"","depth":23,"bounds":{"left":0.10372341,"top":0.6272945,"width":0.3168218,"height":0.030327214},"on_screen":true,"value":"","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Channel product_launches","depth":11,"bounds":{"left":0.0,"top":0.7126895,"width":0.037898935,"height":0.0007980846},"on_screen":true,"role_description":"text"}]...
|
6136536871845528130
|
-6982395231006446076
|
visual_change
|
hybrid
|
NULL
|
Switch workspaces… (Jiminny Inc) Has new messages
Switch workspaces… (Jiminny Inc) Has new messages
Home
Home
DMs
DMs
Activity
Activity
Files
Files
Later
Later
More…
More
Unreads
Threads
Huddles
Drafts & sent
1
Directories
jiminny-x-integration-app
platform-inner-team
ai-chapter
alerts
backend
bugs
confusion-clinic
curiosity_lab
engineering
general
jiminny-bg
platform-tickets
product_launches
random
releases
sofia-office
support
thank-yous
the_people_of_jiminny
Aneliya Angelova
,
Nikolay Yankov
,
Steliyan Georgiev
Stoyan Tanev
Stefka Stoyanova
Ves
Galya Dimitrova
Aneliya Angelova
Vasil Vasilev
James Graham
Nikolay Ivanov
Lukas Kovalik
you
Jira Cloud
Toast
Messages
Messages
Files
Files
Pins
Pins
Add and Edit Channel Tabs
Canvas
List
Folder
Jump to date
Galya Dimitrova
Apr 30th at 3:36:41 PM
3:36 PM
Ask Jiminny Panorama Reports are now live
for all customers that have Panorama chat
For more details please refer to the previous slack post about the feature -
https://jiminny.slack.com/archives/C012P3QDF43/p1776944267826459
https://jiminny.slack.com/archives/C012P3QDF43/p1776944267826459
When the users goes to AI Reports they will see a landing page that gives them details about the feature and shows a video of how the feature works (see attached image).
Emails about the launch will be send to all Admins and Managers from the companies that use AJ Panorama.
Screenshot 2026-04-30 at 15.32.10.png
Toggle file
Screenshot 2026-04-30 at 15.32.10.png
Galya Dimitrova
Galya Dimitrova
Hey team,
Our latest product update is live. Here’s what you need to know:
Ask Jiminny Panorama reports are live [for Jiminny users only]
Why we did it
Managers use AJ Panorama to track what is happening with their team and how they are performing. They want to track this over time and currently they have to go every week and check it manually. We wanted to automate this process for them - send them the report they need straight into their inbox when they need it.
Show more
4 files
Toggle 4 files
Download all
Screenshot 2026-04-23 at 14.21.22.png Screenshot 2026-04-23 at 14.21.22.png PNG
Screenshot 2026-04-23 at 14.21.22.png
PNG
Screenshot 2026-04-23 at 14.21.03.png Screenshot 2026-04-23 at 14.21.03.png PNG
Screenshot 2026-04-23 at 14.21.03.png
PNG
Becky's Objection Handling Tracking - 16 - 22 Apr 2026.pdf PDF
Becky's Objection Handling Tracking - 16 - 22 Apr 2026.pdf
PDF
At-risk Account Sentiment Monitor - 16 - 22 Apr 2026.pdf PDF
At-risk Account Sentiment Monitor - 16 - 22 Apr 2026.pdf
PDF
Thread in product_launches
Thread in
product_launches
|
Apr 23rd
Apr 23rd
|
View message
View message
3 reactions, react with raised hands emoji
3
Add reaction…
Jump to date
New
Adelina Petrova
Today at 2:14:13 PM
2:14 PM
Hey team,
Our latest product update is live. Here’s what you need to know:
Leaderboard View for AI Call Scoring, Coaching Frameworks & Key Words Scoring
Why we did it
Some customers wanted a clearer way to identify top performers and compare team results more easily. Previously, scoring results were harder to scan quickly, especially for managers reviewing large teams.
How it works
• AI Call Scoring, Coaching Frameworks, and Keyword Scoring results now automatically display in leaderboard order
• Top-performing reps appear at the top based on score
• Managers can quickly identify high performers and coaching opportunities
This gives teams better visibility into performance trends and helps surface coaching insights more quickly
Screenshot 2026-05-07 at 12.54.14.png
Toggle file
Screenshot 2026-05-07 at 12.54.14.png
Download Screenshot 2026-05-07 at 12.54.14.png
Share file: Screenshot 2026-05-07 at 12.54.14.png
View canvas details
More actions
3 reactions, react with raised hands emoji
3
Add reaction…
React with white_check_mark
React with eyes
React with raised_hands
Add reaction…
Reply in thread
Forward message…
Save for later
More actions
Channel product_launches
40 hl# Support Daily - in 23 m100% CThu 7 May 14:37:49n a .= custom.log X = laravel.log« SF [jiminny@localhost]HS_local (jiminny@localhost]# console [PKol)A console (eu)« console [STAGING]CrmEntityTrait.phpRateLimit.phpcontiguration.pnpHomeActivityFllesLateMoreSlackViewJiminny...y Starred@jiminny-x-integrati….nolattorm-inner-teamE Channels# ai-chapter# alertsit backend# bugs# confusion-clinid# curiosity lab# engineering# general#jiminny-bgi platform-nckets# product launches# random" released# sofia-office# support# thank-yous# the people of iimi... Direct messages(3 Aneliya Angelova, ...Stoyan Tanev8 Stefka StovanovaGalva Dimitrova> O scriptsv O storage→aoo> M debugbarM frameworkv Mloas.aitianoreê audio. wav= custom.loa=hubsnot-iournal-noll.log= laravel.logus tht isHistoryWindowHelpQ Describe what you are looking for# product launches To communicate new features and products Edit836Messagese Files7 PinsInread in # product_launches Apr 23rd View messageThursday. April 30thv4H38. Hey team,Adelina Petrova 2:14 PMOur larest proouc, updare is live. mere's whar vou need to know09 Leaderboard View for Al Call Scoring, Coaching Frameworks & Key Words ScoringWhy we did itSome customers wanted a clearer way to identify top performers and compare team results more easily. Previously, scoring results werenarder to scan quick v. especiallv for managers reviewing arge teams.How it works• Al Call Scoring, Coaching Frameworks. and Keyword Scoring results now automatically display in leaderboard orden•lop-pertorming reps appear at the top based on score• Managers can quickly idenary high pertormers and coaching opportuninesThis gives teams fetter visibility into performance trends and helps surface coaching insights more quicklyMessage #product launches+ AaluLLuminace supporc racadesLoo..channel channelIlluminate\Support\Facades\Loq::channel( channelum. crue)print risremaining. return: true)):cuscom channel'->intol SincervalPhr cul . pranc.roincerval.retum. crue)'custom_channel')->info('Sbody"PHP_EOl . print_ r(Sbody.return: true:729734 0 >740* athrows HubsnotExcentionnublic function createMeetina(arrav Soavload): Resoonse-...-fo 4 spaces 0...
|
2728
|
NULL
|
NULL
|
NULL
|
|
2731
|
112
|
37
|
2026-05-07T11:37:53.216365+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153873216_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavigarecoderTavsco.s°9 masterProlede PhostormVIewINavigarecoderTavsco.s°9 masterProledey(C) TranscodeParameterResol© RateLimitException.php©UserService.php© Uuid.php> D Traits©RateLimitinterface.php> D UseCases› DUser› DUtils681682› D Validation>Ovophp nelpers.onp© InitialFrontendState.phpc) Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial› D contrib• database, M docsfront-endlangnode_modules library root› [J phpstanM oublic> D resourcesDroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aoi.onopnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage→aoo> D debugbarM frameworkv Mloas.aitianoreD audio.wav= custom.loa© RematchActivityOnCrmObjectDetach.phpDeleteCrmEntityTrait.phpAddkateLimitcommano.php© Hubspot/Client.php x © HandleRateLimit.php© SyncTeamMetadata.php©RateLimit.php© Configuration.php©) ProviderkateLimiter.phgclass Cllent extends Baseclient 1mpLements Hubspotclientintertace6971720723727729* @return array<CrmFieldOption>public function fetch0pportunityFieldOptions(Field $field): arrayf…;* achnows badkequest* achrows hubspoctxcepc1onpublic function makeRequest(string $endpoint, Smethod = 'GET', $payload = [], ?string $queryString = null)Sendpoint = self::BASEURL . Sendpoint.1 Smethod === "GET') <felse{Sresponse = $this->getInstance()->getClient()->request($method, $endpoint, ["json'= (Snavload)ISremaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining');1 "309"Sinterval = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"$body=son decodel(scring) sresponse->gerbodyassociative: true);(Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('$max' . PHP_EOL . print_r($max, return: true));\Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('Sremaining' . PHP_EOL . print_r($remaining, return: true));uuLuminace supporc racades Loo..channel channel'custom_channel')->info('Sinterval ' . PHP_EOL . print_ r(Sinterval. return: true)):ILLuminate Support Facades Loq::channel( channel'custom_channel')->info('$body' . PHP_EOL . print_r($body, return: true));* athrows HubsnotExcention= hubsnot-iournal-noll.log734 0 >nublic function createMeetina(arrav Spavload): Resnonse-...?= laravelllog740us tht is. 50 lilSupport Daily - in 23m100% CThu 7 May 14:37:53E custom.log x = laravel.log4 SF [jiminny@localhost]A HS_Jocal [jiminny@localhost]# console [PKol)A console (eu)« console [STAGING]fo 4 spaces 0...
|
NULL
|
7914772442313094594
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavigarecoderTavsco.s°9 masterProlede PhostormVIewINavigarecoderTavsco.s°9 masterProledey(C) TranscodeParameterResol© RateLimitException.php©UserService.php© Uuid.php> D Traits©RateLimitinterface.php> D UseCases› DUser› DUtils681682› D Validation>Ovophp nelpers.onp© InitialFrontendState.phpc) Jiminny.phpc) Plan.ohoc) serializer.onoC) TeamScimDetails.oho> bootstrar> O build• contial› D contrib• database, M docsfront-endlangnode_modules library root› [J phpstanM oublic> D resourcesDroutesphp api.phpphp api_v2.phppnp console.ongpnp customer_aoi.onopnp embeddea.ongpnp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage→aoo> D debugbarM frameworkv Mloas.aitianoreD audio.wav= custom.loa© RematchActivityOnCrmObjectDetach.phpDeleteCrmEntityTrait.phpAddkateLimitcommano.php© Hubspot/Client.php x © HandleRateLimit.php© SyncTeamMetadata.php©RateLimit.php© Configuration.php©) ProviderkateLimiter.phgclass Cllent extends Baseclient 1mpLements Hubspotclientintertace6971720723727729* @return array<CrmFieldOption>public function fetch0pportunityFieldOptions(Field $field): arrayf…;* achnows badkequest* achrows hubspoctxcepc1onpublic function makeRequest(string $endpoint, Smethod = 'GET', $payload = [], ?string $queryString = null)Sendpoint = self::BASEURL . Sendpoint.1 Smethod === "GET') <felse{Sresponse = $this->getInstance()->getClient()->request($method, $endpoint, ["json'= (Snavload)ISremaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining');1 "309"Sinterval = Sresponse->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"$body=son decodel(scring) sresponse->gerbodyassociative: true);(Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('$max' . PHP_EOL . print_r($max, return: true));\Illuminate\Support\Facades\Log: :channeZ( channel:'custom_channel')->info('Sremaining' . PHP_EOL . print_r($remaining, return: true));uuLuminace supporc racades Loo..channel channel'custom_channel')->info('Sinterval ' . PHP_EOL . print_ r(Sinterval. return: true)):ILLuminate Support Facades Loq::channel( channel'custom_channel')->info('$body' . PHP_EOL . print_r($body, return: true));* athrows HubsnotExcention= hubsnot-iournal-noll.log734 0 >nublic function createMeetina(arrav Spavload): Resnonse-...?= laravelllog740us tht is. 50 lilSupport Daily - in 23m100% CThu 7 May 14:37:53E custom.log x = laravel.log4 SF [jiminny@localhost]A HS_Jocal [jiminny@localhost]# console [PKol)A console (eu)« console [STAGING]fo 4 spaces 0...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2733
|
112
|
38
|
2026-05-07T11:37:57.040595+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778153877040_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.95ms DONE
cache [PASSWORD_DOTS] 9.00ms DONE
compiled [PASSWORD_DOTS] 2.63ms DONE
events [PASSWORD_DOTS] 2.35ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 3.18ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.95ms DONE\n cache ................................................................................................................................ 9.00ms DONE\n compiled ............................................................................................................................. 2.63ms DONE\n events ............................................................................................................................... 2.35ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 3.18ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","depth":4,"bounds":{"left":0.27027926,"top":0.0,"width":0.4800532,"height":1.0},"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.95ms DONE\n cache ................................................................................................................................ 9.00ms DONE\n compiled ............................................................................................................................. 2.63ms DONE\n events ............................................................................................................................... 2.35ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 3.18ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
4477091458398049585
|
3855350223320295684
|
click
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.95ms DONE
cache [PASSWORD_DOTS] 9.00ms DONE
compiled [PASSWORD_DOTS] 2.63ms DONE
events [PASSWORD_DOTS] 2.35ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 3.18ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
2731
|
NULL
|
NULL
|
NULL
|